diff --git a/typescript/openai-agents/src/__tests__/histories/custom-handoff-before-fix.json b/typescript/openai-agents/src/__tests__/histories/custom-handoff-before-fix.json new file mode 100644 index 00000000..3b8128ca --- /dev/null +++ b/typescript/openai-agents/src/__tests__/histories/custom-handoff-before-fix.json @@ -0,0 +1,628 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2026-10-27T03:26:06.673561418Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1048587", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "customHandoffWorkflow" + }, + "taskQueue": { + "name": "custom-handoff-fixture-1793071564962", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InZpcCI=" + } + ] + }, + "workflowExecutionTimeout": "30s", + "workflowRunTimeout": "30s", + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "01a17b64-cf51-788e-a94f-dd93bb7e34fb", + "identity": "144@04f4f6f859c5", + "firstExecutionRunId": "01a17b64-cf51-788e-a94f-dd93bb7e34fb", + "attempt": 1, + "workflowExecutionExpirationTime": "2026-10-27T03:26:36.673Z", + "firstWorkflowTaskBackoff": "0s", + "header": {}, + "workflowId": "custom-handoff-fixture-1793071564962" + } + }, + { + "eventId": "2", + "eventTime": "2026-10-27T03:26:06.673868668Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048588", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "custom-handoff-fixture-1793071564962", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2026-10-27T03:26:06.681904376Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048594", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "144@04f4f6f859c5", + "requestId": "af5bfb25-73db-4a0a-8f74-b7e71ee3684e", + "historySizeBytes": "374", + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + } + } + }, + { + "eventId": "4", + "eventTime": "2026-10-27T03:26:06.788844293Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048599", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "144@04f4f6f859c5", + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + }, + "sdkMetadata": { + "coreUsedFlags": [ + 3, + 1, + 2 + ], + "sdkName": "temporal-typescript", + "sdkVersion": "1.24.0" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2026-10-27T03:26:06.789044335Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1048600", + "activityTaskScheduledEventAttributes": { + "activityId": "1", + "activityType": { + "name": "invokeModelActivity" + }, + "taskQueue": { + "name": "custom-handoff-fixture-1793071564962", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fN2U4OTUyMWE0ZmNjMzgwZDk1YTcxMTA0IiwidHJhY2VJZCI6InRyYWNlXzBlNmU0ZDRlODJiMzgyNGEwNmQ1ODEzZWU0N2YzZDJkIn0=" + } + } + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJtb2RlbE5hbWUiOiJncHQtNG8tbWluaSIsInJlcXVlc3QiOnsic3lzdGVtSW5zdHJ1Y3Rpb25zIjoiIiwiaW5wdXQiOlt7InR5cGUiOiJtZXNzYWdlIiwicm9sZSI6InVzZXIiLCJjb250ZW50IjoiaGVscCJ9XSwibW9kZWxTZXR0aW5ncyI6e30sInRvb2xzIjpbXSwidG9vbHNFeHBsaWNpdGx5UHJvdmlkZWQiOnRydWUsIm91dHB1dFR5cGUiOiJ0ZXh0IiwiaGFuZG9mZnMiOlt7InRvb2xOYW1lIjoidHJhbnNmZXJfdG9fZGVzayIsInRvb2xEZXNjcmlwdGlvbiI6IlJvdXRlIHRvIHRoZSByaWdodCBkZXNrIiwiaW5wdXRKc29uU2NoZW1hIjp7InR5cGUiOiJvYmplY3QiLCJwcm9wZXJ0aWVzIjp7fSwicmVxdWlyZWQiOltdLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9LCJzdHJpY3RKc29uU2NoZW1hIjp0cnVlfV0sInRyYWNpbmciOnRydWUsIm92ZXJyaWRlUHJvbXB0TW9kZWwiOnRydWV9fQ==" + } + ] + }, + "scheduleToCloseTimeout": "30s", + "scheduleToStartTimeout": "30s", + "startToCloseTimeout": "30s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "4", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true + } + }, + { + "eventId": "6", + "eventTime": "2026-10-27T03:26:06.789338335Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1048603", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "5", + "identity": "144@04f4f6f859c5", + "requestId": "e93de853-b01e-415d-a6cb-3ebfa37ee010", + "attempt": 1, + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + } + } + }, + { + "eventId": "7", + "eventTime": "2026-10-27T03:26:06.802194543Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1048604", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ1c2FnZSI6eyJyZXF1ZXN0cyI6MSwiaW5wdXRUb2tlbnMiOjEwLCJvdXRwdXRUb2tlbnMiOjIwLCJ0b3RhbFRva2VucyI6MzAsImlucHV0VG9rZW5zRGV0YWlscyI6W10sIm91dHB1dFRva2Vuc0RldGFpbHMiOltdfSwib3V0cHV0IjpbeyJ0eXBlIjoiZnVuY3Rpb25fY2FsbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19kZXNrIiwiYXJndW1lbnRzIjoie30iLCJjYWxsSWQiOiJjYWxsX2Zha2VfdHJhbnNmZXJfdG9fZGVzayIsInN0YXR1cyI6ImNvbXBsZXRlZCJ9XX0=" + } + ] + }, + "scheduledEventId": "5", + "startedEventId": "6", + "identity": "144@04f4f6f859c5" + } + }, + { + "eventId": "8", + "eventTime": "2026-10-27T03:26:06.802253085Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048605", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "144@04f4f6f859c5-be9bc8cf0af044e28b063af871363c1e", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "custom-handoff-fixture-1793071564962" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "9", + "eventTime": "2026-10-27T03:26:06.803479210Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048609", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "8", + "identity": "144@04f4f6f859c5", + "requestId": "58cb9ce4-afbf-49d8-a0b6-38dbfcb6c922", + "historySizeBytes": "2186", + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + } + } + }, + { + "eventId": "10", + "eventTime": "2026-10-27T03:26:06.819121460Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048614", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "8", + "startedEventId": "9", + "identity": "144@04f4f6f859c5", + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "11", + "eventTime": "2026-10-27T03:26:06.819175751Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1048615", + "activityTaskScheduledEventAttributes": { + "activityId": "2", + "activityType": { + "name": "invokeModelActivity" + }, + "taskQueue": { + "name": "custom-handoff-fixture-1793071564962", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fOTYxN2FmNmQxMDIyZmVjZGJkY2Q1NzFmIiwidHJhY2VJZCI6InRyYWNlXzBlNmU0ZDRlODJiMzgyNGEwNmQ1ODEzZWU0N2YzZDJkIn0=" + } + } + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJtb2RlbE5hbWUiOiJncHQtNG8tbWluaSIsInJlcXVlc3QiOnsic3lzdGVtSW5zdHJ1Y3Rpb25zIjoiIiwiaW5wdXQiOlt7InR5cGUiOiJtZXNzYWdlIiwicm9sZSI6InVzZXIiLCJjb250ZW50IjoiaGVscCJ9LHsidHlwZSI6ImZ1bmN0aW9uX2NhbGwiLCJuYW1lIjoidHJhbnNmZXJfdG9fZGVzayIsImFyZ3VtZW50cyI6Int9IiwiY2FsbElkIjoiY2FsbF9mYWtlX3RyYW5zZmVyX3RvX2Rlc2siLCJzdGF0dXMiOiJjb21wbGV0ZWQifSx7InR5cGUiOiJmdW5jdGlvbl9jYWxsX3Jlc3VsdCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19kZXNrIiwiY2FsbElkIjoiY2FsbF9mYWtlX3RyYW5zZmVyX3RvX2Rlc2siLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJvdXRwdXQiOnsidHlwZSI6InRleHQiLCJ0ZXh0Ijoie1wiYXNzaXN0YW50XCI6XCJzdGFuZGFyZFwifSJ9fV0sIm1vZGVsU2V0dGluZ3MiOnt9LCJ0b29scyI6W3sidHlwZSI6ImZ1bmN0aW9uIiwibmFtZSI6InN0YW5kYXJkRGVzayIsImRlc2NyaXB0aW9uIjoiTG9vayB1cCB0aGUgc3RhbmRhcmREZXNrIGRlc2siLCJwYXJhbWV0ZXJzIjp7InR5cGUiOiJvYmplY3QiLCJwcm9wZXJ0aWVzIjp7fSwicmVxdWlyZWQiOltdLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9LCJzdHJpY3QiOnRydWV9XSwidG9vbHNFeHBsaWNpdGx5UHJvdmlkZWQiOnRydWUsIm91dHB1dFR5cGUiOiJ0ZXh0IiwiaGFuZG9mZnMiOltdLCJ0cmFjaW5nIjp0cnVlLCJvdmVycmlkZVByb21wdE1vZGVsIjp0cnVlfX0=" + } + ] + }, + "scheduleToCloseTimeout": "30s", + "scheduleToStartTimeout": "30s", + "startToCloseTimeout": "30s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "10", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true + } + }, + { + "eventId": "12", + "eventTime": "2026-10-27T03:26:06.819193043Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1048617", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "11", + "identity": "144@04f4f6f859c5", + "requestId": "18928143-c3f5-41b1-947e-73e56c642a56", + "attempt": 1, + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + } + } + }, + { + "eventId": "13", + "eventTime": "2026-10-27T03:26:06.824585085Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1048618", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ1c2FnZSI6eyJyZXF1ZXN0cyI6MSwiaW5wdXRUb2tlbnMiOjEwLCJvdXRwdXRUb2tlbnMiOjIwLCJ0b3RhbFRva2VucyI6MzAsImlucHV0VG9rZW5zRGV0YWlscyI6W10sIm91dHB1dFRva2Vuc0RldGFpbHMiOltdfSwib3V0cHV0IjpbeyJ0eXBlIjoiZnVuY3Rpb25fY2FsbCIsIm5hbWUiOiJzdGFuZGFyZERlc2siLCJhcmd1bWVudHMiOiJ7fSIsImNhbGxJZCI6ImNhbGxfZmFrZV9zdGFuZGFyZERlc2siLCJzdGF0dXMiOiJjb21wbGV0ZWQifV19" + } + ] + }, + "scheduledEventId": "11", + "startedEventId": "12", + "identity": "144@04f4f6f859c5" + } + }, + { + "eventId": "14", + "eventTime": "2026-10-27T03:26:06.824598043Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048619", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "144@04f4f6f859c5-be9bc8cf0af044e28b063af871363c1e", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "custom-handoff-fixture-1793071564962" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "15", + "eventTime": "2026-10-27T03:26:06.825781168Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048623", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "14", + "identity": "144@04f4f6f859c5", + "requestId": "db01ba40-77bd-4798-9582-fbe7b9d4dfd4", + "historySizeBytes": "4259", + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + } + } + }, + { + "eventId": "16", + "eventTime": "2026-10-27T03:26:06.833963960Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048628", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "14", + "startedEventId": "15", + "identity": "144@04f4f6f859c5", + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "17", + "eventTime": "2026-10-27T03:26:06.833995126Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1048629", + "activityTaskScheduledEventAttributes": { + "activityId": "3", + "activityType": { + "name": "standardDesk" + }, + "taskQueue": { + "name": "custom-handoff-fixture-1793071564962", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fNTgxMTg5YTA3OGQ4ZDg2ZDI0Mzc1NTgwIiwidHJhY2VJZCI6InRyYWNlXzBlNmU0ZDRlODJiMzgyNGEwNmQ1ODEzZWU0N2YzZDJkIn0=" + } + } + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "e30=" + } + ] + }, + "scheduleToCloseTimeout": "30s", + "scheduleToStartTimeout": "30s", + "startToCloseTimeout": "30s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "16", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true + } + }, + { + "eventId": "18", + "eventTime": "2026-10-27T03:26:06.834009960Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1048631", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "17", + "identity": "144@04f4f6f859c5", + "requestId": "166a2823-fbc4-4729-b6fc-0a50e4a443d0", + "attempt": 1, + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + } + } + }, + { + "eventId": "19", + "eventTime": "2026-10-27T03:26:06.836230001Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1048632", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InN0YW5kYXJkIGRlc2si" + } + ] + }, + "scheduledEventId": "17", + "startedEventId": "18", + "identity": "144@04f4f6f859c5" + } + }, + { + "eventId": "20", + "eventTime": "2026-10-27T03:26:06.836238293Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048633", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "144@04f4f6f859c5-be9bc8cf0af044e28b063af871363c1e", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "custom-handoff-fixture-1793071564962" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "21", + "eventTime": "2026-10-27T03:26:06.837167460Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048637", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "20", + "identity": "144@04f4f6f859c5", + "requestId": "958768f3-7e99-4232-a11b-5f863b013409", + "historySizeBytes": "5321", + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + } + } + }, + { + "eventId": "22", + "eventTime": "2026-10-27T03:26:06.843864793Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048642", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "20", + "startedEventId": "21", + "identity": "144@04f4f6f859c5", + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "23", + "eventTime": "2026-10-27T03:26:06.843889335Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1048643", + "activityTaskScheduledEventAttributes": { + "activityId": "4", + "activityType": { + "name": "invokeModelActivity" + }, + "taskQueue": { + "name": "custom-handoff-fixture-1793071564962", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fYjdjMWJjMTBiMjA5OTllNGYwMTNhNTFjIiwidHJhY2VJZCI6InRyYWNlXzBlNmU0ZDRlODJiMzgyNGEwNmQ1ODEzZWU0N2YzZDJkIn0=" + } + } + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJtb2RlbE5hbWUiOiJncHQtNG8tbWluaSIsInJlcXVlc3QiOnsic3lzdGVtSW5zdHJ1Y3Rpb25zIjoiIiwiaW5wdXQiOlt7InR5cGUiOiJtZXNzYWdlIiwicm9sZSI6InVzZXIiLCJjb250ZW50IjoiaGVscCJ9LHsidHlwZSI6ImZ1bmN0aW9uX2NhbGwiLCJuYW1lIjoidHJhbnNmZXJfdG9fZGVzayIsImFyZ3VtZW50cyI6Int9IiwiY2FsbElkIjoiY2FsbF9mYWtlX3RyYW5zZmVyX3RvX2Rlc2siLCJzdGF0dXMiOiJjb21wbGV0ZWQifSx7InR5cGUiOiJmdW5jdGlvbl9jYWxsX3Jlc3VsdCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19kZXNrIiwiY2FsbElkIjoiY2FsbF9mYWtlX3RyYW5zZmVyX3RvX2Rlc2siLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJvdXRwdXQiOnsidHlwZSI6InRleHQiLCJ0ZXh0Ijoie1wiYXNzaXN0YW50XCI6XCJzdGFuZGFyZFwifSJ9fSx7InR5cGUiOiJmdW5jdGlvbl9jYWxsIiwibmFtZSI6InN0YW5kYXJkRGVzayIsImFyZ3VtZW50cyI6Int9IiwiY2FsbElkIjoiY2FsbF9mYWtlX3N0YW5kYXJkRGVzayIsInN0YXR1cyI6ImNvbXBsZXRlZCJ9LHsidHlwZSI6ImZ1bmN0aW9uX2NhbGxfcmVzdWx0IiwibmFtZSI6InN0YW5kYXJkRGVzayIsImNhbGxJZCI6ImNhbGxfZmFrZV9zdGFuZGFyZERlc2siLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJvdXRwdXQiOnsidHlwZSI6InRleHQiLCJ0ZXh0Ijoic3RhbmRhcmQgZGVzayJ9fV0sIm1vZGVsU2V0dGluZ3MiOnt9LCJ0b29scyI6W3sidHlwZSI6ImZ1bmN0aW9uIiwibmFtZSI6InN0YW5kYXJkRGVzayIsImRlc2NyaXB0aW9uIjoiTG9vayB1cCB0aGUgc3RhbmRhcmREZXNrIGRlc2siLCJwYXJhbWV0ZXJzIjp7InR5cGUiOiJvYmplY3QiLCJwcm9wZXJ0aWVzIjp7fSwicmVxdWlyZWQiOltdLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9LCJzdHJpY3QiOnRydWV9XSwidG9vbHNFeHBsaWNpdGx5UHJvdmlkZWQiOnRydWUsIm91dHB1dFR5cGUiOiJ0ZXh0IiwiaGFuZG9mZnMiOltdLCJ0cmFjaW5nIjp0cnVlLCJvdmVycmlkZVByb21wdE1vZGVsIjp0cnVlfX0=" + } + ] + }, + "scheduleToCloseTimeout": "30s", + "scheduleToStartTimeout": "30s", + "startToCloseTimeout": "30s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "22", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true + } + }, + { + "eventId": "24", + "eventTime": "2026-10-27T03:26:06.843902126Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1048645", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "23", + "identity": "144@04f4f6f859c5", + "requestId": "8fa8c61a-7c24-4f4e-8e24-2c1a251c341a", + "attempt": 1, + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + } + } + }, + { + "eventId": "25", + "eventTime": "2026-10-27T03:26:06.846124126Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1048646", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ1c2FnZSI6eyJyZXF1ZXN0cyI6MSwiaW5wdXRUb2tlbnMiOjEwLCJvdXRwdXRUb2tlbnMiOjE1LCJ0b3RhbFRva2VucyI6MjUsImlucHV0VG9rZW5zRGV0YWlscyI6W10sIm91dHB1dFRva2Vuc0RldGFpbHMiOltdfSwib3V0cHV0IjpbeyJ0eXBlIjoibWVzc2FnZSIsImlkIjoibXNnX2Zha2VfMDAxIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOlt7InR5cGUiOiJvdXRwdXRfdGV4dCIsInRleHQiOiJzdGFuZGFyZCBhbnN3ZXIifV0sInN0YXR1cyI6ImNvbXBsZXRlZCJ9XX0=" + } + ] + }, + "scheduledEventId": "23", + "startedEventId": "24", + "identity": "144@04f4f6f859c5" + } + }, + { + "eventId": "26", + "eventTime": "2026-10-27T03:26:06.846134460Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048647", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "144@04f4f6f859c5-be9bc8cf0af044e28b063af871363c1e", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "custom-handoff-fixture-1793071564962" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "27", + "eventTime": "2026-10-27T03:26:06.847182043Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048651", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "26", + "identity": "144@04f4f6f859c5", + "requestId": "e9fdf211-9fcd-4123-83b5-8a007fc3d603", + "historySizeBytes": "7690", + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + } + } + }, + { + "eventId": "28", + "eventTime": "2026-10-27T03:26:06.856326126Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048655", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "26", + "startedEventId": "27", + "identity": "144@04f4f6f859c5", + "workerVersion": { + "buildId": "@temporalio/worker@1.24.0+1d59385e28b0929ecbb717f5d79775abe528040dac445b2c10fc93211a276bc4" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "29", + "eventTime": "2026-10-27T03:26:06.856485126Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1048656", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InN0YW5kYXJkOiBzdGFuZGFyZCBhbnN3ZXIi" + } + ] + }, + "workflowTaskCompletedEventId": "28" + } + } + ] +} diff --git a/typescript/openai-agents/src/__tests__/test-openai-agents.ts b/typescript/openai-agents/src/__tests__/test-openai-agents.ts index cf660dee..c6081ca4 100644 --- a/typescript/openai-agents/src/__tests__/test-openai-agents.ts +++ b/typescript/openai-agents/src/__tests__/test-openai-agents.ts @@ -1,10 +1,13 @@ // Integration tests that need a real Worker. Pure-function tests live in // the sibling `*.test.ts` files. +import { readFileSync } from 'node:fs'; +import path from 'node:path'; import { setTracingDisabled } from '@openai/agents-core'; import type { MCPServer } from '@openai/agents-core'; import { WorkflowClient } from '@temporalio/client'; import { temporal } from '@temporalio/proto'; import { helpers } from '@temporalio/test-helpers'; +import { Worker } from '@temporalio/worker'; import { WorkflowStreamClient } from '@temporalio/workflow-streams/client'; import { OpenAIAgentsPlugin, @@ -13,6 +16,7 @@ import { StatelessMCPServerProvider, } from '..'; import { + customHandoffWorkflow, localActivityAgentWorkflow, mcpFactoryArgWorkflow, summaryOverrideStringWorkflow, @@ -25,7 +29,7 @@ import { streamingNoTopicWorkflow, } from './workflows/openai-agents'; import { makeTestFunction } from './helpers/test-fn'; -import { FakeModelProvider, textResponse, toolCallResponse } from './stubs/openai-agents'; +import { FakeModelProvider, handoffResponse, textResponse, toolCallResponse } from './stubs/openai-agents'; import { StreamingFakeModelProvider, streamingTextEvents } from './stubs/openai-agents-fakes'; import EventType = temporal.api.enums.v1.EventType; @@ -524,3 +528,46 @@ test('Streaming: useLocalActivity fails fast before scheduling an Activity', asy t.is(result, 'StreamingLocalActivityUnsupported'); }); }); + +const deskActivities = { + standardDesk: async () => 'standard desk', + vipDesk: async () => 'vip desk', +}; + +test('A custom Handoff hands off to the agent its callback returns', async (t) => { + const { createWorker, executeWorkflow } = helpers(t); + const modelProvider = new FakeModelProvider([ + handoffResponse('transfer_to_desk'), + textResponse('vip answer'), + handoffResponse('transfer_to_desk'), + textResponse('standard answer'), + ]); + const worker = await createWorker({ + plugins: [new OpenAIAgentsPlugin({ modelProvider })], + activities: deskActivities, + }); + + const results = await worker.runUntil(async () => [ + await executeWorkflow(customHandoffWorkflow, { args: ['vip'], workflowExecutionTimeout: '30 seconds' }), + await executeWorkflow(customHandoffWorkflow, { args: ['standard'], workflowExecutionTimeout: '30 seconds' }), + ]); + + t.deepEqual(results, ['vip: vip answer', 'standard: standard answer']); +}); + +test('A custom Handoff keeps its agent field when replaying a run recorded before the fix', async (t) => { + // Recorded with the plugin before this change: the callback returned vip, but the run went to + // standard, whose model called its standardDesk tool. + const history = JSON.parse( + readFileSync(path.resolve(__dirname, '../../src/__tests__/histories/custom-handoff-before-fix.json'), 'utf8') + ); + await t.notThrowsAsync( + Worker.runReplayHistory( + { + workflowBundle: t.context.workflowBundle, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new FakeModelProvider([]) })], + }, + history + ) + ); +}); diff --git a/typescript/openai-agents/src/__tests__/test-openai-sandbox.ts b/typescript/openai-agents/src/__tests__/test-openai-sandbox.ts index 4dcab3fd..5435dd76 100644 --- a/typescript/openai-agents/src/__tests__/test-openai-sandbox.ts +++ b/typescript/openai-agents/src/__tests__/test-openai-sandbox.ts @@ -4,9 +4,11 @@ import { helpers } from '@temporalio/test-helpers'; import { OpenAIAgentsPlugin, OpenAIAgentsTraceClientInterceptor, SandboxClientProvider } from '..'; import { installAgentSdkCollector } from './helpers/openai-agents-spans'; import { makeTestFunction } from './helpers/test-fn'; -import { FakeModelProvider, textResponse, toolCallResponse } from './stubs/openai-agents'; +import { FakeModelProvider, handoffResponse, textResponse, toolCallResponse } from './stubs/openai-agents'; import { FakeSandboxClient, FakeSandboxSession } from './stubs/sandbox-fakes'; import { + customHandoffSandboxAgentWithoutConfigWorkflow, + customHandoffSandboxToolWorkflow, sandboxAgentWorkflow, sandboxApprovalResumeWorkflow, sandboxArchiveLimitsWorkflow, @@ -223,3 +225,43 @@ test('addTemporalSpans: sandbox exec emits sandbox:* spans carrying only safe me await agentSdk.teardown(); } }); + +for (const inGraph of [false, true]) { + test(`A custom Handoff target ${inGraph ? 'also in the handoff graph' : 'returned only by the callback'} gets the sandbox config`, async (t) => { + const { createWorker, executeWorkflow } = helpers(t); + const session = new FakeSandboxSession(); + const client = new FakeSandboxClient(session); + const worker = await createWorker({ + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new FakeModelProvider([ + handoffResponse('transfer_to_desk'), + toolCallResponse('ask_sandbox', { input: 'go' }), + toolCallResponse('run_command', { cmd: 'echo hello' }), + textResponse('sandbox done'), + textResponse('vip answer'), + ]), + sandboxClientProviders: [new SandboxClientProvider('fake', client)], + }), + ], + }); + + await worker.runUntil(async () => { + t.is(await executeWorkflow(customHandoffSandboxToolWorkflow, { args: [inGraph] }), 'vip: vip answer'); + }); + t.is(session.execCalls[0]?.cmd, 'echo hello'); + }); +} + +test('A custom Handoff returning a SandboxAgent without runConfig.sandbox fails the Workflow', async (t) => { + const { createWorker, executeWorkflow } = helpers(t); + const worker = await createWorker({ + plugins: [new OpenAIAgentsPlugin({ modelProvider: new FakeModelProvider([handoffResponse('transfer_to_desk')]) })], + }); + + await worker.runUntil(async () => { + const err = await t.throwsAsync(executeWorkflow(customHandoffSandboxAgentWithoutConfigWorkflow)); + t.regex(String((err as any)?.cause?.message), /runConfig\.sandbox is not configured/); + t.is((err as any)?.cause?.type, 'SandboxConfigurationError'); + }); +}); diff --git a/typescript/openai-agents/src/__tests__/workflows/openai-agents.ts b/typescript/openai-agents/src/__tests__/workflows/openai-agents.ts index b739c0d1..443634dd 100644 --- a/typescript/openai-agents/src/__tests__/workflows/openai-agents.ts +++ b/typescript/openai-agents/src/__tests__/workflows/openai-agents.ts @@ -1,7 +1,7 @@ -import { Agent } from '@openai/agents-core'; +import { Agent, Handoff } from '@openai/agents-core'; import { ApplicationFailure } from '@temporalio/workflow'; import { WorkflowStream } from '@temporalio/workflow-streams/workflow'; -import { TemporalOpenAIRunner, statelessMcpServer, statefulMcpServer } from '../../workflow'; +import { TemporalOpenAIRunner, activityAsTool, statelessMcpServer, statefulMcpServer } from '../../workflow'; export interface StreamingAgentResult { finalOutput: string; @@ -176,3 +176,28 @@ export async function statefulMcpSlowConnectHeartbeatWorkflow(): Promise await server.cleanup(); } } + +/** + * A custom `Handoff` whose callback picks the desk from the run context, as the + * upstream handoffs guide describes. Each desk has its own Activity tool. + */ +export async function customHandoffWorkflow(tier: string): Promise { + const deskTool = (name: string) => + activityAsTool<(input: Record) => Promise>({ + name, + description: `Look up the ${name} desk`, + parameters: { type: 'object', properties: {}, required: [], additionalProperties: false }, + }); + const standard = new Agent<{ tier: string }>({ + name: 'standard', + model: 'gpt-4o-mini', + tools: [deskTool('standardDesk')], + }); + const vip = new Agent<{ tier: string }>({ name: 'vip', model: 'gpt-4o-mini', tools: [deskTool('vipDesk')] }); + const desk = new Handoff(standard, async (runContext) => (runContext.context.tier === 'vip' ? vip : standard)); + desk.toolName = 'transfer_to_desk'; + desk.toolDescription = 'Route to the right desk'; + const router = new Agent<{ tier: string }>({ name: 'router', model: 'gpt-4o-mini', handoffs: [desk] }); + const result = await new TemporalOpenAIRunner().run(router, 'help', { context: { tier } }); + return `${result.lastAgent?.name}: ${result.finalOutput}`; +} diff --git a/typescript/openai-agents/src/__tests__/workflows/openai-sandbox.ts b/typescript/openai-agents/src/__tests__/workflows/openai-sandbox.ts index f259645d..bc9efedd 100644 --- a/typescript/openai-agents/src/__tests__/workflows/openai-sandbox.ts +++ b/typescript/openai-agents/src/__tests__/workflows/openai-sandbox.ts @@ -1,4 +1,4 @@ -import { Agent, type FunctionTool, type RunContext, type Tool } from '@openai/agents-core'; +import { Agent, Handoff, type FunctionTool, type RunContext, type Tool } from '@openai/agents-core'; import { Capability, Manifest, @@ -6,7 +6,7 @@ import { isEnvValueReference, type SandboxSessionLike, } from '@openai/agents-core/sandbox'; -import { TemporalOpenAIRunner, workerEnvValue, temporalSandboxClient } from '../../workflow'; +import { TemporalOpenAIRunner, agentAsTool, workerEnvValue, temporalSandboxClient } from '../../workflow'; class TestSandboxCapability extends Capability { readonly type = 'test_sandbox'; @@ -203,3 +203,39 @@ export async function sandboxValidationWorkflow(): Promise { return 'OK'; } + +/** + * A custom `Handoff` whose callback returns `vip`, which asks a nested `SandboxAgent` through + * `agentAsTool`. With `inGraph`, `vip` is also a handoff of the router, so the runner sees it up front. + */ +export async function customHandoffSandboxToolWorkflow(inGraph: boolean): Promise { + const specialist = new SandboxAgent({ + name: 'sandbox-specialist', + model: 'gpt-4o-mini', + capabilities: [new TestSandboxCapability()], + }); + const vip = new Agent({ + name: 'vip', + model: 'gpt-4o-mini', + tools: [agentAsTool(specialist, { toolName: 'ask_sandbox' })], + }); + const standard = new Agent({ name: 'standard', model: 'gpt-4o-mini' }); + const desk = new Handoff(standard, async () => vip); + desk.toolName = 'transfer_to_desk'; + const router = new Agent({ name: 'router', model: 'gpt-4o-mini', handoffs: inGraph ? [desk, vip] : [desk] }); + const result = await new TemporalOpenAIRunner().run(router, 'help', { + runConfig: { sandbox: { client: temporalSandboxClient('fake') } }, + }); + return `${result.lastAgent?.name}: ${result.finalOutput}`; +} + +/** A custom `Handoff` whose callback returns a `SandboxAgent`, with no `runConfig.sandbox`. */ +export async function customHandoffSandboxAgentWithoutConfigWorkflow(): Promise { + const specialist = new SandboxAgent({ name: 'sandbox-specialist', model: 'gpt-4o-mini' }); + const standard = new Agent({ name: 'standard', model: 'gpt-4o-mini' }); + const desk = new Handoff(standard, async () => specialist); + desk.toolName = 'transfer_to_desk'; + const router = new Agent({ name: 'router', model: 'gpt-4o-mini', handoffs: [desk] }); + await new TemporalOpenAIRunner().run(router, 'help'); + return 'FAIL: should have thrown'; +} diff --git a/typescript/openai-agents/src/workflow/convert-agent.ts b/typescript/openai-agents/src/workflow/convert-agent.ts index c699db37..34684079 100644 --- a/typescript/openai-agents/src/workflow/convert-agent.ts +++ b/typescript/openai-agents/src/workflow/convert-agent.ts @@ -1,18 +1,55 @@ import { type Agent, Handoff } from '@openai/agents-core'; +import { SandboxAgent, type SandboxRunConfig } from '@openai/agents-core/sandbox'; import { ApplicationFailure } from '@temporalio/common'; +import { patched } from '@temporalio/workflow'; import type { ModelActivityOptions } from '../common/model-activity-options'; import { ActivityBackedModel } from './activity-backed-model'; +/** + * Runs that handed off to a custom `Handoff`'s `agent` field, when its callback + * returned another agent, keep doing so on replay: that agent's model call and + * tools would not match their history. + */ +const HANDOFF_RETURNED_AGENT_PATCH = 'openai-agents.handoff-returned-agent'; + +const agentAsToolSandboxConfig = Symbol.for('@temporalio/openai-agents/agentAsToolSandboxConfig'); + +/** Whether a `SandboxAgent` is reachable from `agent` through its handoff graph. */ +function hasSandboxAgent(agent: Agent, seen: Set> = new Set()): boolean { + if (agent instanceof SandboxAgent) return true; + if (seen.has(agent)) return false; + seen.add(agent); + for (const handoff of agent.handoffs ?? []) { + const target = handoff instanceof Handoff ? handoff.agent : handoff; + if (hasSandboxAgent(target, seen)) return true; + } + return false; +} + +/** Fails when a `SandboxAgent` is reachable from `agent` and `runConfig.sandbox` is not set. */ +export function requireSandboxRunConfig(agent: Agent, sandbox: SandboxRunConfig | undefined): void { + if (sandbox !== undefined || !hasSandboxAgent(agent)) return; + throw ApplicationFailure.create({ + message: + 'A SandboxAgent was provided but runConfig.sandbox is not configured. ' + + 'Set runConfig.sandbox with a client created via temporalSandboxClient(name) ' + + 'from @temporalio/openai-agents/workflow.', + type: 'SandboxConfigurationError', + nonRetryable: true, + }); +} + /** * Recursively converts an agent graph: validate tools, replace each agent's - * model with an ActivityBackedModel, and clone handoffs. Uses a seen-map to - * handle circular handoff references. + * model with an ActivityBackedModel, pass `sandbox` to its `agentAsTool` tools, + * and clone handoffs. Uses a seen-map to handle circular handoff references. */ export function convertAgent( agent: Agent, modelParams: ModelActivityOptions, seen?: Map, Agent>, - modelNameOverride?: string + modelNameOverride?: string, + sandbox?: SandboxRunConfig ): Agent { seen = seen ?? new Map(); if (seen.has(agent)) return seen.get(agent)!; @@ -59,13 +96,34 @@ export function convertAgent( const converted = agent.clone({ model: activityBackedModel }); seen.set(agent, converted); + if (sandbox !== undefined) { + converted.tools = converted.tools.map((tool) => { + if (tool.type !== 'function') return tool; + const configureSandbox = ( + tool as typeof tool & { [agentAsToolSandboxConfig]?: (sandbox: SandboxRunConfig) => typeof tool } + )[agentAsToolSandboxConfig]; + if (typeof configureSandbox !== 'function') return tool; + return configureSandbox(sandbox); + }); + } const convertedHandoffs = (agent.handoffs ?? []).map((h) => { if (h instanceof Handoff) { - const convertedHandoffAgent = convertAgent(h.agent, modelParams, seen, modelNameOverride); - return h.clone({ agent: convertedHandoffAgent }); + const convertedHandoffAgent = convertAgent(h.agent, modelParams, seen, modelNameOverride, sandbox); + // The run hands off to the agent the callback returns, which a custom + // Handoff can choose at invocation time, so check and convert that agent + // as the runner does the initial graph. + return h.clone({ + agent: convertedHandoffAgent, + onInvokeHandoff: async (context, args) => { + const target = await h.onInvokeHandoff(context, args); + if (target === h.agent || !patched(HANDOFF_RETURNED_AGENT_PATCH)) return convertedHandoffAgent; + requireSandboxRunConfig(target, sandbox); + return convertAgent(target, modelParams, seen, modelNameOverride, sandbox); + }, + }); } - return convertAgent(h, modelParams, seen, modelNameOverride); + return convertAgent(h, modelParams, seen, modelNameOverride, sandbox); }); converted.handoffs = convertedHandoffs; diff --git a/typescript/openai-agents/src/workflow/runner.ts b/typescript/openai-agents/src/workflow/runner.ts index 732cfedc..ae826ca2 100644 --- a/typescript/openai-agents/src/workflow/runner.ts +++ b/typescript/openai-agents/src/workflow/runner.ts @@ -1,7 +1,6 @@ import { type Agent, type AgentInputItem, - Handoff, MemorySession, Runner, RunState, @@ -17,7 +16,7 @@ import { type StreamedRunResult, type TracingConfig, } from '@openai/agents-core'; -import { SandboxAgent, type SandboxRunConfig } from '@openai/agents-core/sandbox'; +import type { SandboxRunConfig } from '@openai/agents-core/sandbox'; import { ApplicationFailure } from '@temporalio/common'; import { DEFAULT_MODEL_ACTIVITY_OPTIONS, @@ -27,14 +26,12 @@ import { type SerializableModelActivityOptions, } from '../common/model-activity-options'; import { unwrapTemporalFailure } from '../common/errors'; -import { convertAgent } from './convert-agent'; +import { convertAgent, requireSandboxRunConfig } from './convert-agent'; import { TemporalSandboxClient } from './sandbox-client'; import { ensureTracingProcessorRegistered } from './tracing'; import { flushOpenSpans } from './agent-sink-processor'; import { getCurrentPluginConfig } from './plugin-config-store'; -const agentAsToolSandboxConfig = Symbol.for('@temporalio/openai-agents/agentAsToolSandboxConfig'); - export interface TemporalRunOptions { /** Run context passed to agents and tools */ context?: TContext; @@ -133,34 +130,13 @@ function definedFields(obj: T | undefined): Partial { return result; } -/** Whether a `SandboxAgent` is reachable from `agent` through its handoff graph. */ -function hasSandboxAgent(agent: Agent, seen: Set> = new Set()): boolean { - if (agent instanceof SandboxAgent) return true; - if (seen.has(agent)) return false; - seen.add(agent); - for (const handoff of agent.handoffs ?? []) { - const target = handoff instanceof Handoff ? handoff.agent : handoff; - if (hasSandboxAgent(target, seen)) return true; - } - return false; -} - /** * `runConfig.sandbox.client` must be a `TemporalSandboxClient` so every sandbox * operation is dispatched as an Activity rather than run inline in the Workflow. */ function validateSandboxRunConfig(agent: Agent, sandbox: SandboxRunConfig | undefined): void { - if (!hasSandboxAgent(agent) && sandbox === undefined) return; - if (sandbox === undefined) { - throw ApplicationFailure.create({ - message: - 'A SandboxAgent was provided but runConfig.sandbox is not configured. ' + - 'Set runConfig.sandbox with a client created via temporalSandboxClient(name) ' + - 'from @temporalio/openai-agents/workflow.', - type: 'SandboxConfigurationError', - nonRetryable: true, - }); - } + requireSandboxRunConfig(agent, sandbox); + if (sandbox === undefined) return; if (sandbox.client == null) { throw ApplicationFailure.create({ message: @@ -181,26 +157,6 @@ function validateSandboxRunConfig(agent: Agent, sandbox: SandboxRunCon } } -function propagateAgentToolSandboxConfig( - agent: Agent, - sandbox: SandboxRunConfig, - seen: Set> = new Set() -): void { - if (seen.has(agent)) return; - seen.add(agent); - agent.tools = agent.tools.map((tool) => { - if (tool.type !== 'function') return tool; - const configureSandbox = ( - tool as typeof tool & { [agentAsToolSandboxConfig]?: (sandbox: SandboxRunConfig) => typeof tool } - )[agentAsToolSandboxConfig]; - if (typeof configureSandbox !== 'function') return tool; - return configureSandbox(sandbox); - }); - for (const handoff of agent.handoffs ?? []) { - propagateAgentToolSandboxConfig(handoff instanceof Handoff ? handoff.agent : handoff, sandbox, seen); - } -} - /** * A Temporal-aware agent runner that delegates model calls to Activities. * @@ -271,10 +227,7 @@ export class TemporalOpenAIRunner { const { model: modelOverride, ...runnerConfigOverrides } = options?.runConfig ?? {}; - const converted = convertAgent(agent, this.modelParams, undefined, modelOverride); - if (options?.runConfig?.sandbox !== undefined) { - propagateAgentToolSandboxConfig(converted, options.runConfig.sandbox); - } + const converted = convertAgent(agent, this.modelParams, undefined, modelOverride, options?.runConfig?.sandbox); let preparedInput: string | AgentInputItem[] | RunState; if (input instanceof RunState) {