@temporalio/agent-harness 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/README.md +534 -5
  2. package/dist/src/harness/agent-protocol/agent-interface.d.ts +11 -0
  3. package/dist/src/harness/agent-protocol/agent-interface.js +14 -0
  4. package/dist/src/harness/agent-workflow/types.d.ts +3 -0
  5. package/dist/src/harness/agent-workflow.d.ts +12 -1
  6. package/dist/src/harness/agent-workflow.js +106 -35
  7. package/dist/src/harness/agent.d.ts +7 -0
  8. package/dist/src/harness/agent.js +8 -2
  9. package/package.json +6 -4
  10. package/skills/temporal-harness-typescript/SKILL.md +42 -0
  11. package/skills/temporal-harness-typescript/agents/openai.yaml +4 -0
  12. package/skills/temporal-harness-typescript/assets/starter/README.md.tmpl +95 -0
  13. package/skills/temporal-harness-typescript/assets/starter/agents.toml.tmpl +7 -0
  14. package/skills/temporal-harness-typescript/assets/starter/agents.ts.tmpl +61 -0
  15. package/skills/temporal-harness-typescript/assets/starter/env.example.tmpl +3 -0
  16. package/skills/temporal-harness-typescript/assets/starter/gitignore.tmpl +6 -0
  17. package/skills/temporal-harness-typescript/assets/starter/package.json.tmpl +35 -0
  18. package/skills/temporal-harness-typescript/assets/starter/tsconfig.json.tmpl +13 -0
  19. package/skills/temporal-harness-typescript/assets/starter/worker.ts.tmpl +24 -0
  20. package/skills/temporal-harness-typescript/references/advanced.md +57 -0
  21. package/skills/temporal-harness-typescript/references/api.md +110 -0
  22. package/skills/temporal-harness-typescript/references/authoring.md +65 -0
  23. package/skills/temporal-harness-typescript/references/verification.md +17 -0
@@ -6,9 +6,10 @@
6
6
  // Ambient context (Python's contextvars) is AsyncLocalStorage: the participant's message id is
7
7
  // set once per handler task, and the in-flight tool call (runner, tool id, injections) is set by
8
8
  // `runTool` for exactly one invocation. Both are read only inside this package.
9
- import { ApplicationFailure, AsyncLocalStorage, CancellationScope, ParentClosePolicy, allHandlersFinished, condition, defineQuery, defineSignal, defineUpdate, getExternalWorkflowHandle, isCancellation, log, proxyActivities, setHandler, startChild, uuid4, ActivityFailure, } from "@temporalio/workflow";
9
+ import { ApplicationFailure, AsyncLocalStorage, CancellationScope, ParentClosePolicy, allHandlersFinished, condition, defineQuery, defineSignal, defineUpdate, getExternalWorkflowHandle, log, proxyActivities, setHandler, startChild, uuid4, ActivityFailure, } from "@temporalio/workflow";
10
+ import { msToNumber } from "@temporalio/common/lib/time.js";
10
11
  import { WorkflowStream } from "@temporalio/workflow-streams/workflow";
11
- import { AGENT_ID_LENGTH, AGENT_INTERFACE_QUERY, AGENT_STATUS_QUERY, APPROVAL_SHORT_ID_LENGTH, AgentMessageSchema, AutoApprovalCriteria, ApprovalVerdict, WIRE_VERDICT, CLOSE_SIGNAL, CallbackResultSchema, DEFAULT_SUBAGENT_HEARTBEAT_TIMEOUT, DEFAULT_SUBAGENT_START_TO_CLOSE_TIMEOUT, MessageDisposition, MidTurn, PROVIDE_CALLBACK_RESULT_UPDATE, SEND_AGENT_MESSAGE_UPDATE, TOOL_APPROVAL_UPDATE, TURN_EVENTS_TOPIC, ToolApprovalDecisionSchema, ToolApprovalPolicy, toJsonSchema, validate, } from "./agent-protocol/index.js";
12
+ import { AGENT_ID_LENGTH, AGENT_INTERFACE_QUERY, AGENT_STATUS_QUERY, APPROVAL_SHORT_ID_LENGTH, AgentMessageSchema, AutoApprovalCriteria, ApprovalVerdict, WIRE_VERDICT, CLOSE_SIGNAL, CallbackResultSchema, CloseReason, DEFAULT_SUBAGENT_HEARTBEAT_TIMEOUT, DEFAULT_SUBAGENT_START_TO_CLOSE_TIMEOUT, MessageDisposition, MidTurn, PROVIDE_CALLBACK_RESULT_UPDATE, SEND_AGENT_MESSAGE_UPDATE, TOOL_APPROVAL_UPDATE, TURN_EVENTS_TOPIC, ToolApprovalDecisionSchema, ToolApprovalPolicy, toJsonSchema, validate, } from "./agent-protocol/index.js";
12
13
  import { StateRef } from "./state/index.js";
13
14
  import { CallbackToolError, ToolApprovalDenied, assertAutoModeHasEvaluator, assertPortableJson, errorMessage, errorText, isOwnCancellation, } from "./agent-workflow/errors.js";
14
15
  export { CallbackToolError, ToolApprovalDenied, errorText, };
@@ -53,7 +54,10 @@ export class AgentWorkflowRunner {
53
54
  #events;
54
55
  #handlers;
55
56
  #evaluator;
57
+ // Milliseconds idle before the runner closes the agent; null never closes on idle.
58
+ #closeAfterIdleMs;
56
59
  #closed = false;
60
+ #closeReason = null;
57
61
  #currentTurn = 0;
58
62
  #currentTurnId = null;
59
63
  #participants = 0;
@@ -74,6 +78,15 @@ export class AgentWorkflowRunner {
74
78
  options.autoApprovalCriteriaDefault ??
75
79
  AutoApprovalCriteria.empty();
76
80
  this.agentId = config.agent_id ?? shortHex(AGENT_ID_LENGTH);
81
+ const closeAfterIdleMs = config.close_after_idle !== null
82
+ ? config.close_after_idle * 1000
83
+ : options.closeAfterIdleDefault === undefined
84
+ ? 0
85
+ : msToNumber(options.closeAfterIdleDefault);
86
+ if (closeAfterIdleMs < 0)
87
+ throw new TypeError("closeAfterIdleDefault must not be negative; use 0 or omit it for never");
88
+ // 0 is "never", kept as null from here on so there is one spelling.
89
+ this.#closeAfterIdleMs = closeAfterIdleMs > 0 ? closeAfterIdleMs : null;
77
90
  this.stream = new WorkflowStream();
78
91
  this.#events = this.stream.topic(TURN_EVENTS_TOPIC);
79
92
  // Before any handler is installed: a buffered first message is dispatched by setHandler, and
@@ -95,9 +108,7 @@ export class AgentWorkflowRunner {
95
108
  });
96
109
  setHandler(agentStatusQuery, () => this.status);
97
110
  setHandler(agentInterfaceQuery, () => this.#agentInterface());
98
- setHandler(closeSignal, () => {
99
- this.#closed = true;
100
- });
111
+ setHandler(closeSignal, () => this.#close(CloseReason.REQUESTED));
101
112
  }
102
113
  // -- public surface -------------------------------------------------------------------------
103
114
  get closed() {
@@ -178,6 +189,7 @@ export class AgentWorkflowRunner {
178
189
  })),
179
190
  approval_policy: this.#policy,
180
191
  has_auto_approval_evaluator: this.#evaluator !== undefined,
192
+ close_reason: this.#closeReason,
181
193
  };
182
194
  }
183
195
  /** Publish a payload on the current turn, attributed to the ambient message. */
@@ -203,40 +215,64 @@ export class AgentWorkflowRunner {
203
215
  }
204
216
  // -- the turn loop --------------------------------------------------------------------------
205
217
  async run(impls) {
206
- try {
207
- while (!this.#closed) {
208
- await condition(() => this.#canOpenTurn ||
209
- this.#joining.length > 0 ||
210
- this.#closed);
211
- if (this.#closed)
212
- break;
213
- for (let joined = this.#joining.shift(); joined; joined = this.#joining.shift()) {
214
- void this.#runParticipant(impls, joined, true);
215
- }
216
- if (this.#canOpenTurn) {
217
- const admitted = this.#pendingTurns.shift();
218
- this.#currentTurn += 1;
219
- this.#currentTurnId = admitted.turn_id;
220
- this.#participants = 1;
221
- this.pub(admitted.turn_id, admitted.turn_number, { type: "turn_started" }, null);
222
- void this.#runParticipant(impls, admitted, false);
223
- }
218
+ while (!this.#closed) {
219
+ await this.#waitForWork();
220
+ if (this.#closed)
221
+ break;
222
+ for (let joined = this.#joining.shift(); joined; joined = this.#joining.shift()) {
223
+ void this.#runParticipant(impls, joined, true);
224
224
  }
225
- await condition(() => this.#participants === 0);
226
- }
227
- catch (e) {
228
- if (isCancellation(e)) {
229
- await CancellationScope.nonCancellable(async () => {
230
- this.stream.detachPollers();
231
- await condition(allHandlersFinished);
232
- });
225
+ if (this.#canOpenTurn) {
226
+ const admitted = this.#pendingTurns.shift();
227
+ this.#currentTurn += 1;
228
+ this.#currentTurnId = admitted.turn_id;
229
+ this.#participants = 1;
230
+ this.pub(admitted.turn_id, admitted.turn_number, { type: "turn_started" }, null);
231
+ void this.#runParticipant(impls, admitted, false);
233
232
  }
234
- throw e;
235
233
  }
236
- // Parked polls are handed the last events (turn_end included) before the run completes.
234
+ await condition(() => this.#participants === 0);
235
+ if (this.#closeReason !== null)
236
+ this.pub("", 0, { type: "agent_closed", reason: this.#closeReason }, null);
237
+ }
238
+ /**
239
+ * @internal The last step of a run, after teardown: parked stream polls are handed the last
240
+ * events, then every handler finishes. Not before teardown, which may publish too, and whose
241
+ * whole duration the stream must stay readable for: a poll arriving once pollers are detached
242
+ * is refused.
243
+ */
244
+ async finish() {
237
245
  this.stream.detachPollers();
238
246
  await condition(allHandlersFinished);
239
247
  }
248
+ /**
249
+ * Wait until there is something for the turn loop to do, or the agent closes. With
250
+ * `close_after_idle` set this is also the idle clock: while the agent is idle the wait carries
251
+ * that timeout, and running out closes the agent; while a turn runs the wait also wakes when the
252
+ * agent goes idle, so the loop comes back here and starts the clock afresh.
253
+ */
254
+ async #waitForWork() {
255
+ const hasWork = () => this.#canOpenTurn || this.#joining.length > 0 || this.#closed;
256
+ if (this.#closeAfterIdleMs === null) {
257
+ await condition(hasWork);
258
+ }
259
+ else if (!this.#idle) {
260
+ await condition(() => hasWork() || this.#idle);
261
+ }
262
+ else if (!(await condition(hasWork, this.#closeAfterIdleMs)) && this.#idle) {
263
+ // The idle check covers a message admitted in the same workflow task as the timer firing.
264
+ this.#close(CloseReason.IDLE_TIMEOUT_EXPIRED);
265
+ }
266
+ }
267
+ /** Stop admitting messages and let the turn loop exit. The first reason sticks. */
268
+ #close(reason) {
269
+ this.#closeReason ??= reason;
270
+ this.#closed = true;
271
+ }
272
+ /** No turn running, nothing queued, no join waiting to be dispatched. */
273
+ get #idle() {
274
+ return !this.#hasPendingWork && this.#joining.length === 0;
275
+ }
240
276
  // -- the stream -----------------------------------------------------------------------------
241
277
  // WorkflowStream keeps its log private; the runner reads its head offset from it. Revisit if the
242
278
  // SDK exposes one.
@@ -301,6 +337,15 @@ export class AgentWorkflowRunner {
301
337
  }
302
338
  // -- send_agent_message ---------------------------------------------------------------------
303
339
  #validateSendAgentMessage(raw) {
340
+ // A closed agent's loop has exited, so an admitted message would never run.
341
+ if (this.#closeReason !== null) {
342
+ throw ApplicationFailure.create({
343
+ type: "AgentClosed",
344
+ nonRetryable: true,
345
+ message: `The agent is closed (${this.#closeReason}) and accepts no more messages.`,
346
+ details: [{ reason: this.#closeReason }],
347
+ });
348
+ }
304
349
  const message = AgentMessageSchema.parse(raw);
305
350
  assertPortableJson(message.payload, `Payload for function '${message.type}'`);
306
351
  const handler = Object.hasOwn(this.#handlers, message.type)
@@ -672,13 +717,17 @@ export class AgentWorkflowRunner {
672
717
  }, entry.message_id);
673
718
  }
674
719
  // -- subagents ------------------------------------------------------------------------------
675
- /** Start a child agent; returns its short, tree-unique handle (also its agent_id). */
720
+ /**
721
+ * Start a child agent; returns its short, tree-unique handle (also its agent_id). The child never
722
+ * closes on idle: `close_after_idle` is always pushed down as 0, overriding both `config` and the
723
+ * child's own default.
724
+ */
676
725
  async startSubagent(agentKey, workflowType, taskQueue, { config = {}, data } = {}) {
677
726
  let handle = `${this.agentId}-${shortHex(AGENT_ID_LENGTH)}`;
678
727
  while (this.#subagents.has(handle))
679
728
  handle = `${this.agentId}-${shortHex(AGENT_ID_LENGTH)}`;
680
729
  const workflowId = `${agentKey}-subagent-${uuid4()}`;
681
- const childConfig = { ...config, agent_id: handle };
730
+ const childConfig = { ...config, agent_id: handle, close_after_idle: 0 };
682
731
  await startChild(workflowType, {
683
732
  args: data === undefined ? [childConfig] : [childConfig, data],
684
733
  workflowId,
@@ -761,6 +810,17 @@ export class AgentWorkflowRunner {
761
810
  (cause.type === "SubagentTurnError" || cause.type === "SubagentNoReply")) {
762
811
  this.#publishReplyReceived(inst, msgType, acceptedTurnFromError(cause), "error");
763
812
  }
813
+ if (cause instanceof ApplicationFailure && cause.type === "AgentClosed") {
814
+ // Something other than this parent closed or ended the child, so the handle is dead.
815
+ this.#forgetClosedSubagent(inst);
816
+ throw ApplicationFailure.create({
817
+ type: "AgentClosed",
818
+ nonRetryable: true,
819
+ message: `Subagent '${handle}' has closed and accepts no more messages. Start a new one with start_${inst.agent_key}.`,
820
+ details: [{ subagent_id: handle }],
821
+ cause,
822
+ });
823
+ }
764
824
  throw e;
765
825
  }
766
826
  inst.last_consumed_offset = result.consumed_offset;
@@ -771,6 +831,17 @@ export class AgentWorkflowRunner {
771
831
  inst.serving += 1;
772
832
  }
773
833
  }
834
+ /** Drop a subagent something else closed or ended; only the first send to find it publishes. */
835
+ #forgetClosedSubagent(inst) {
836
+ if (!this.#subagents.delete(inst.handle))
837
+ return;
838
+ this.publish({
839
+ type: "subagent_stopped",
840
+ subagent_id: inst.handle,
841
+ agent_key: inst.agent_key,
842
+ workflow_id: inst.workflow_id,
843
+ });
844
+ }
774
845
  #publishReplyReceived(inst, handler, subagentTurn, outcome) {
775
846
  this.publish({
776
847
  type: "subagent_reply_received",
@@ -1,4 +1,5 @@
1
1
  import { type ActivityOptions } from "@temporalio/workflow";
2
+ import type { Duration } from "@temporalio/common";
2
3
  import { z } from "zod";
3
4
  import { MidTurn, ToolApprovalPolicy, type AgentConfig, type InferInput, type InferOutput, type JsonValue, type Schema, type AutoApprovalCriteria, type AutoModeEvaluator, type TurnStreamContext } from "./agent-protocol/index.ts";
4
5
  import { AgentWorkflowRunner, type HandlerDecl, type HandlerImpl } from "./agent-workflow.ts";
@@ -124,6 +125,12 @@ export interface AgentDefinition<H extends Record<string, HandlerDecl> = Record<
124
125
  approvalPolicyDefault: ToolApprovalPolicy;
125
126
  autoApprovalCriteriaDefault?: AutoApprovalCriteria;
126
127
  autoModeEvaluator?: AutoModeEvaluator;
128
+ /**
129
+ * Close the agent once it has been idle (no turn running, nothing queued) this long, publishing
130
+ * `agent_closed` with reason `idle_timeout_expired`. Omitted or 0 never closes on idle. A
131
+ * caller's `AgentConfig.close_after_idle` (seconds; 0 for never) overrides it.
132
+ */
133
+ closeAfterIdleDefault?: Duration;
127
134
  create(ctx: AgentContext<S, D>): AgentImpl<H>;
128
135
  }
129
136
  export type AgentWorkflow<Def extends AgentDefinition = AgentDefinition> = ((config?: unknown, data?: unknown) => Promise<void>) & {
@@ -235,6 +235,7 @@ export function defineAgent(definition) {
235
235
  approvalPolicyDefault: definition.approvalPolicyDefault,
236
236
  autoApprovalCriteriaDefault: definition.autoApprovalCriteriaDefault,
237
237
  autoModeEvaluator: definition.autoModeEvaluator,
238
+ closeAfterIdleDefault: definition.closeAfterIdleDefault,
238
239
  states: definition.states,
239
240
  data,
240
241
  });
@@ -254,11 +255,16 @@ export function defineAgent(definition) {
254
255
  await runner.run(impl);
255
256
  }
256
257
  catch (e) {
257
- if (isCancellation(e) && impl.teardown)
258
- await CancellationScope.nonCancellable(impl.teardown);
258
+ if (isCancellation(e)) {
259
+ await CancellationScope.nonCancellable(async () => {
260
+ await impl.teardown?.();
261
+ await runner.finish();
262
+ });
263
+ }
259
264
  throw e;
260
265
  }
261
266
  await impl.teardown?.();
267
+ await runner.finish();
262
268
  };
263
269
  Object.defineProperty(workflow, "name", { value: definition.name });
264
270
  return Object.assign(workflow, { definition });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temporalio/agent-harness",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Temporal-native agent harness for TypeScript: the workflow-safe authoring API for durable agents.",
@@ -49,14 +49,16 @@
49
49
  "./ai-sdks/openai-agents-harness": {
50
50
  "types": "./dist/src/ai-sdks/openai-agents-harness.d.ts",
51
51
  "default": "./dist/src/ai-sdks/openai-agents-harness.js"
52
- }
52
+ },
53
+ "./skill": "./skills/temporal-harness-typescript/SKILL.md"
53
54
  },
54
55
  "files": [
55
- "dist/src"
56
+ "dist/src",
57
+ "skills"
56
58
  ],
57
59
  "dependencies": {
58
60
  "@standard-schema/spec": "^1.1.0",
59
- "@temporalio/agent-harness-client": "^0.2.0",
61
+ "@temporalio/agent-harness-client": "^0.3.0",
60
62
  "@temporalio/common": "^1.24.0",
61
63
  "@temporalio/workflow": "^1.24.0",
62
64
  "@temporalio/workflow-streams": "^1.24.0",
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: temporal-harness-typescript
3
+ description: Build, run, test, and integrate TypeScript agents using the Temporal Agent Harness SDK and shared harness API. Use for agent authoring, tools, approvals, state, session clients, or startup troubleshooting in applications using this SDK.
4
+ ---
5
+
6
+ # Temporal Agent Harness for TypeScript
7
+
8
+ Build against the installed package's public API. Read its version before choosing imports and
9
+ integration dependencies. These resources ship with the SDK; no repository checkout is required.
10
+ Preserve the application's chosen model provider and framework. The bundled OpenAI starter is an
11
+ example, not a requirement to replace an existing integration.
12
+
13
+ ## Choose the task
14
+
15
+ - New agent or worker: read [authoring](references/authoring.md), then the bundled starter,
16
+ which is installed next to this file and is the complete worked example for this release:
17
+ [assets/starter/agents.ts.tmpl](assets/starter/agents.ts.tmpl) (agent, handlers, tool) and
18
+ [assets/starter/worker.ts.tmpl](assets/starter/worker.ts.tmpl) (worker and plugins). Adapt it rather
19
+ than re-deriving the wiring. The package's `init` command materializes it as a new project.
20
+ - Connect an application, stream replies, handle approvals, or generate client types: read
21
+ [the shared API guide](references/api.md).
22
+ - State, Code Mode, callback tools, or subagents: read [advanced features](references/advanced.md).
23
+ - Validate behavior or troubleshoot startup: read [verification](references/verification.md).
24
+
25
+ Inside the harness repository checkout itself, use the `temporal-harness-repo` skill in its
26
+ `.agents/skills/` instead: there, Bazel and `just` replace these commands and registries are generated.
27
+
28
+ Load only the references relevant to the task. Use schemas and public types to discover the
29
+ agent's interface before searching implementation files. The project may also have its own
30
+ instructions; these resources describe SDK usage, not permission to change deployment settings.
31
+
32
+ ## Runtime boundaries
33
+
34
+ An agent runs as a Temporal workflow on your TypeScript worker. The console, HTTP API, and
35
+ session-manager worker currently come from the Python package. All services must connect to the
36
+ same Temporal namespace; worker queues must match `agents.toml`. Provider API keys belong on the
37
+ agent worker. Model integration plugins precede `AgentHarnessPlugin`.
38
+
39
+ Keep external I/O out of workflow code. Register activity tool bodies on the worker, regenerate
40
+ schemas after changing declarations, and verify a complete session using installed packages.
41
+ Verify with the starter's `ping` handler first: it needs no model call or provider key.
42
+ Do not import from repository-only `examples` or tests in an application.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Temporal Harness TypeScript"
3
+ short_description: "Build durable TypeScript agents and harness API clients"
4
+ default_prompt: "Use $temporal-harness-typescript to build and verify this harness application."
@@ -0,0 +1,95 @@
1
+ # Your TypeScript harness agent
2
+
3
+ This project was generated by `agent-harness init` from @temporalio/agent-harness
4
+ __HARNESS_VERSION__. You need Node 22.18+, npm, uv, the Temporal CLI, and an OpenAI API key for
5
+ the `ask` handler. The weather tool returns sample data.
6
+
7
+ ## 1. Install the generated project
8
+
9
+ Project creation is already complete. Run the following from this directory:
10
+
11
+ ```bash
12
+ npm install
13
+ ```
14
+
15
+ `agents.ts` defines your agent, `worker.ts` runs it, and `agents.toml` registers it with the
16
+ console. `package.json` pins the harness, compatible Temporal/model SDK versions, and the Python
17
+ package that provides the shared services.
18
+
19
+ ## 2. Configure it and install the skill
20
+
21
+ ```bash
22
+ cp env.example .env.local
23
+ npm run schema
24
+ npm run harness:skill # Codex reads .agents/skills
25
+ npx agent-harness skills install --for claude # Claude Code reads .claude/skills
26
+ ```
27
+
28
+ `.env.local` is where `OPENAI_API_KEY` goes once you try `ask`; never commit it. `schema`
29
+ compiles the agent and writes `agent.schema.json`. `harness:skill` copies this SDK release's skill, with
30
+ authoring, API, advanced-feature, and verification references, into the directory your coding
31
+ agent reads. Run it for each tool you use. For another tool, run `npx agent-harness skills path`
32
+ to locate the bundled resources, or use `npx agent-harness skills install --dest <directory>`.
33
+
34
+ ## 3. Start the three services
35
+
36
+ Run each command in its own terminal, all from this project directory:
37
+
38
+ **Terminal 1 — Temporal service** (skip if your configured Temporal service is already running):
39
+
40
+ ```bash
41
+ temporal server start-dev
42
+ ```
43
+
44
+ **Terminal 2 — shared session manager, API, and harness console:**
45
+
46
+ ```bash
47
+ npm run harness:dev
48
+ ```
49
+
50
+ **Terminal 3 — your TypeScript agent worker:**
51
+
52
+ ```bash
53
+ npm start
54
+ ```
55
+
56
+ **The API, console, and session manager come from the Python package.** `harness:dev` runs them
57
+ with `uvx` in one process; no Python project of your own is needed, and the console is prebuilt.
58
+ Your agent worker runs in Node. The Python version is pinned in `package.json`.
59
+
60
+ Defaults are `localhost:7233` and namespace `default`. For another Temporal service, export the
61
+ same `TEMPORAL_*` connection settings in each service terminal. `.env.local` loads only into the
62
+ Node worker. Leave `TASK_QUEUE_PREFIX` unset for this starter. Keep all three processes running.
63
+
64
+ ## 4. Verify, then develop with your coding agent
65
+
66
+ 1. Open the **harness console at <http://localhost:8000>**. Create a session for
67
+ **OpenAI Hello (TypeScript)**.
68
+ 2. Send `ping` a text message. Expect the same text back without a model request or an API key.
69
+ 3. Set `OPENAI_API_KEY` in `.env.local`, restart the worker, then send `ask`
70
+ **“What's the weather in Paris?”**. Expect a streamed reply and a completed `get_weather`
71
+ tool trace with sample weather data. This step calls OpenAI.
72
+ 4. Reload the page and confirm the session reconnects.
73
+ 5. Check startup and worker availability:
74
+
75
+ ```bash
76
+ npm run harness:doctor
77
+ ```
78
+
79
+ Temporal's own workflow UI is at <http://localhost:8233>; it is separate from the harness console.
80
+
81
+ With the services running, open the project in your coding agent. Reload its session if it was
82
+ already running, then ask it to use the skill:
83
+
84
+ | Tool | Prompt |
85
+ | --- | --- |
86
+ | Codex | `Use $temporal-harness-typescript to help me build this agent. Explain the starter, then verify it with ping before making model calls.` |
87
+ | Claude Code | `/temporal-harness-typescript Explain the starter, then verify it with ping before making model calls.` |
88
+
89
+ Edit `agents.ts` to change your agent and `worker.ts` for worker configuration. After changes,
90
+ stop the worker, run `npm run schema`, and restart `npm start`. Use a new session for changed
91
+ workflow behavior. Commit `agent.schema.json` and `package-lock.json`.
92
+
93
+ After upgrading the SDK, rerun the `skills install` commands to update unchanged skill files.
94
+ Local edits produce a conflict and are preserved. Application-client guidance is in the installed
95
+ skill's `references/api.md`.
@@ -0,0 +1,7 @@
1
+ [[agents]]
2
+ key = "openai-hello-ts"
3
+ workflow_type = "OpenAIHelloAgentTs"
4
+ task_queue = "openai-hello-ts"
5
+ label = "OpenAI Hello (TypeScript)"
6
+ description = "Chat with a TypeScript agent and try its sample weather tool."
7
+ schema = "agent.schema.json"
@@ -0,0 +1,61 @@
1
+ import { Agent, type AgentInputItem } from "@openai/agents-core";
2
+ import { z } from "zod";
3
+ import {
4
+ TextMessage,
5
+ TextReply,
6
+ ToolApprovalPolicy,
7
+ defineAgent,
8
+ handler,
9
+ tool,
10
+ } from "@temporalio/agent-harness";
11
+ import {
12
+ asOpenAIAgentTool,
13
+ runStreamed,
14
+ } from "@temporalio/agent-harness/ai-sdks/openai-agents-harness";
15
+
16
+ const getWeather = tool({
17
+ name: "get_weather",
18
+ description: "Return sample weather for a city.",
19
+ inputSchema: z.object({ city: z.string() }),
20
+ outputSchema: z.string(),
21
+ inherentlySafe: true,
22
+ execute: async ({ city }) => `It's 72°F and sunny in ${city}.`,
23
+ });
24
+
25
+ export const OpenAIHelloAgentTs = defineAgent({
26
+ name: "OpenAIHelloAgentTs",
27
+ approvalPolicyDefault: ToolApprovalPolicy.allowInherentlySafe(),
28
+ handlers: {
29
+ ping: handler({ description: "Check the worker without a model call.", input: TextMessage, output: TextReply }),
30
+ ask: handler({
31
+ description: "Chat with the assistant. Ask about the weather to try its tool.",
32
+ input: TextMessage,
33
+ output: TextReply,
34
+ }),
35
+ },
36
+ create: ({ runner }) => {
37
+ let conversation: AgentInputItem[] = [];
38
+ return {
39
+ async ping({ text }) { return { text }; },
40
+ async ask({ text }) {
41
+ const sdkAgent = new Agent({
42
+ name: "Hello",
43
+ instructions: "Answer briefly. For weather questions, call get_weather and explain that it returns sample data.",
44
+ model: "gpt-5.1",
45
+ tools: [asOpenAIAgentTool(runner, getWeather)],
46
+ });
47
+ const input: AgentInputItem[] = [...conversation, { role: "user", content: text }];
48
+ const result = await runStreamed(runner, sdkAgent, input, {
49
+ defaultModelParams: {
50
+ startToCloseTimeout: "2 minutes",
51
+ heartbeatTimeout: "30 seconds",
52
+ },
53
+ });
54
+ for await (const _event of result) void _event;
55
+ await result.completed;
56
+ conversation = result.history;
57
+ return { text: String(result.finalOutput) };
58
+ },
59
+ };
60
+ },
61
+ });
@@ -0,0 +1,3 @@
1
+ # Uncomment and set your key before trying `ask`; `ping` works without one.
2
+ # OPENAI_API_KEY=sk-...
3
+ # Export the same TEMPORAL_* settings in every service terminal when using a profile.
@@ -0,0 +1,6 @@
1
+ .env.local
2
+ .venv/
3
+ __pycache__/
4
+ *.egg-info/
5
+ node_modules/
6
+ dist/
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "tsc -p tsconfig.json",
7
+ "schema": "npm run build && agent-harness schema ./dist/agents.js#OpenAIHelloAgentTs -o agent.schema.json",
8
+ "harness:dev": "uvx --from 'temporal-agent-harness[ui]==0.9.0' temporal-agent-harness dev agents.toml",
9
+ "start": "node --env-file-if-exists=.env.local dist/worker.js",
10
+ "harness:skill": "agent-harness skills install",
11
+ "harness:doctor": "agent-harness doctor --schema agent.schema.json"
12
+ },
13
+ "dependencies": {
14
+ "@temporalio/agent-harness": "__HARNESS_VERSION__",
15
+ "@temporalio/agent-harness-worker": "__HARNESS_VERSION__",
16
+ "@temporalio/agent-harness-client": "__HARNESS_VERSION__",
17
+ "@temporalio/activity": "1.24.0",
18
+ "@temporalio/client": "1.24.0",
19
+ "@temporalio/common": "1.24.0",
20
+ "@temporalio/envconfig": "1.24.0",
21
+ "@temporalio/plugin": "1.24.0",
22
+ "@temporalio/worker": "1.24.0",
23
+ "@temporalio/workflow": "1.24.0",
24
+ "@temporalio/workflow-streams": "1.24.0",
25
+ "@temporalio/openai-agents": "1.24.0",
26
+ "@openai/agents-core": "0.14.3",
27
+ "@openai/agents-openai": "0.14.3",
28
+ "openai": "6.46.0",
29
+ "zod": "4.6.5"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "7.0.2",
33
+ "@types/node": "22.20.4"
34
+ }
35
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "types": ["node"],
9
+ "rootDir": ".",
10
+ "outDir": "dist"
11
+ },
12
+ "include": ["agents.ts", "worker.ts"]
13
+ }
@@ -0,0 +1,24 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { loadClientConnectConfig } from "@temporalio/envconfig";
3
+ import { NativeConnection, Worker } from "@temporalio/worker";
4
+ import { AgentHarnessPlugin } from "@temporalio/agent-harness-worker";
5
+ import { OpenAIAgentsPlugin } from "@temporalio/agent-harness-worker/ai-sdks/openai-agents";
6
+
7
+ // `ping` needs no model, so the worker starts without a key; only `ask` fails without one.
8
+ if (!process.env.OPENAI_API_KEY)
9
+ console.warn("OPENAI_API_KEY is not set: `ping` works, `ask` will fail until it is (see env.example)");
10
+
11
+ const { connectionOptions, namespace } = loadClientConnectConfig();
12
+ const connection = await NativeConnection.connect(connectionOptions);
13
+ try {
14
+ const worker = await Worker.create({
15
+ connection,
16
+ namespace: namespace ?? "default",
17
+ taskQueue: "openai-hello-ts",
18
+ workflowsPath: fileURLToPath(new URL("./agents.js", import.meta.url)),
19
+ plugins: [new OpenAIAgentsPlugin(), new AgentHarnessPlugin()],
20
+ });
21
+ await worker.run();
22
+ } finally {
23
+ await connection.close();
24
+ }