@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.
- package/README.md +534 -5
- package/dist/src/harness/agent-protocol/agent-interface.d.ts +11 -0
- package/dist/src/harness/agent-protocol/agent-interface.js +14 -0
- package/dist/src/harness/agent-workflow/types.d.ts +3 -0
- package/dist/src/harness/agent-workflow.d.ts +12 -1
- package/dist/src/harness/agent-workflow.js +106 -35
- package/dist/src/harness/agent.d.ts +7 -0
- package/dist/src/harness/agent.js +8 -2
- package/package.json +6 -4
- package/skills/temporal-harness-typescript/SKILL.md +42 -0
- package/skills/temporal-harness-typescript/agents/openai.yaml +4 -0
- package/skills/temporal-harness-typescript/assets/starter/README.md.tmpl +95 -0
- package/skills/temporal-harness-typescript/assets/starter/agents.toml.tmpl +7 -0
- package/skills/temporal-harness-typescript/assets/starter/agents.ts.tmpl +61 -0
- package/skills/temporal-harness-typescript/assets/starter/env.example.tmpl +3 -0
- package/skills/temporal-harness-typescript/assets/starter/gitignore.tmpl +6 -0
- package/skills/temporal-harness-typescript/assets/starter/package.json.tmpl +35 -0
- package/skills/temporal-harness-typescript/assets/starter/tsconfig.json.tmpl +13 -0
- package/skills/temporal-harness-typescript/assets/starter/worker.ts.tmpl +24 -0
- package/skills/temporal-harness-typescript/references/advanced.md +57 -0
- package/skills/temporal-harness-typescript/references/api.md +110 -0
- package/skills/temporal-harness-typescript/references/authoring.md +65 -0
- 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,
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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)
|
|
258
|
-
await CancellationScope.nonCancellable(
|
|
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.
|
|
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.
|
|
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,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,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,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
|
+
}
|