@temporalio/agent-harness-codegen 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Temporal Technologies Inc. All rights reserved.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,76 @@
1
- # Temporary Holding Version
1
+ # @temporalio/agent-harness-codegen
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Generate TypeScript application types from a Python or TypeScript harness agent's JSON Schema.
4
+ The generator is a public npm package starting with the next npm release; it was previously
5
+ repository-only tooling. No checkout or Bazel installation is needed to use the package.
6
+
7
+ ```bash
8
+ npm install --save-dev --save-exact @temporalio/agent-harness-codegen@0.3.0
9
+ # Export the schema using the SDK that defines your agent:
10
+ uv run temporal-agent-harness schema my_agent.workflow:OpenAIHelloAgentWorkflow -o agent.schema.json
11
+ # or: npx agent-harness-schema ./dist/agents.js#OpenAIHelloAgentTs -o agent.schema.json
12
+ npx harness-codegen agent.schema.json -o Agent.ts
13
+ ```
14
+
15
+ The version above tracks this development branch and becomes installable when released.
16
+ Use the same release version as your other npm harness packages.
17
+
18
+ The output has an interface per model the agent exposes (its `@agent.accepts` input and output
19
+ models and its `agent.state(...)` types) and one mapping type naming them:
20
+
21
+ ```ts
22
+ export interface TicTacToeAgent {
23
+ handlers: {
24
+ new_game: { input: NewGame; output: TextReply };
25
+ play: { input: PlayMove; output: TextReply };
26
+ };
27
+ states: {
28
+ board: Board;
29
+ };
30
+ }
31
+ ```
32
+
33
+ Handler inputs are typed as a sender writes them (a field with a default is optional); handler
34
+ outputs and states as they arrive on the wire (every field present). Tool inputs and outputs are
35
+ not typed.
36
+
37
+ Options: `-o <file>` writes to a file instead of stdout; `--name <TypeName>` renames the mapping
38
+ type, which defaults to the agent's workflow type name.
39
+
40
+ `--json-schemas` also writes `<TypeName>Schemas`: each handler's input and output as JSON Schema,
41
+ so you can check a message or a reply at runtime with any validator. Each schema is
42
+ self-contained (a `$ref` plus just the `$defs` it reaches), and the constant is `as const` for
43
+ tools that derive types from schemas. For example, with Ajv:
44
+
45
+ ```ts
46
+ import Ajv from "ajv";
47
+ import { TicTacToeAgentSchemas } from "./TicTacToeAgent";
48
+
49
+ const validPlay = new Ajv().compile(TicTacToeAgentSchemas.play.input);
50
+ if (!validPlay(move)) console.warn(validPlay.errors);
51
+ ```
52
+
53
+ It is off by default, so the generated files examples commit stay as they are.
54
+
55
+ ## The event protocol
56
+
57
+ With `--protocol`, it reads `temporal-agent-harness schema --protocol` instead and writes the
58
+ event stream's types. The client package (`typescript/packages/client`) commits that output as
59
+ its `src/protocol.ts`; `bazel run //typescript/packages/client/src:protocol_codegen_update` regenerates it.
60
+
61
+ ## Development
62
+
63
+ ```sh
64
+ bazel test //typescript/packages/codegen/... # one target per test file, e.g. //typescript/packages/codegen/test:generate_test
65
+ ```
66
+
67
+ Without Bazel, from this directory:
68
+
69
+ ```sh
70
+ pnpm install
71
+ pnpm test # compiles src/ and test/ (including compile-time type checks), then runs node --test
72
+ ```
73
+
74
+ `test/golden/TicTacToeAgent.ts` is generated from the tic-tac-toe example's committed schema,
75
+ `python/examples/tictactoe/TicTacToeAgent.schema.json`;
76
+ `bazel run //typescript/packages/codegen/test/golden:golden_update` regenerates it.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ // harness-codegen <schema.json | -> [-o <out.ts>] [--name TypeName] [--json-schemas] [--protocol]
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
+ import { parseArgs } from "node:util";
5
+ import { generateAgentTypes, generateProtocolTypes } from "./generate.js";
6
+ const USAGE = `usage: harness-codegen <schema.json | -> [-o <out.ts>] [--name <TypeName>] [--json-schemas] [--protocol]
7
+
8
+ Reads the document \`temporal-agent-harness schema module:Class\` prints (from a file, or
9
+ stdin for -) and writes TypeScript types for the agent: one interface per model, and a
10
+ mapping type naming each handler's input and output and each declared state. With
11
+ --json-schemas, also writes each handler's input and output JSON Schema as a constant.
12
+
13
+ With --protocol, reads \`temporal-agent-harness schema --protocol\` instead and writes the
14
+ event stream's types.`;
15
+ async function main() {
16
+ const { values, positionals } = parseArgs({
17
+ allowPositionals: true,
18
+ options: {
19
+ output: { type: "string", short: "o" },
20
+ name: { type: "string" },
21
+ protocol: { type: "boolean" },
22
+ "json-schemas": { type: "boolean" },
23
+ help: { type: "boolean", short: "h" }
24
+ }
25
+ });
26
+ if (values.help || positionals.length !== 1) {
27
+ console.error(USAGE);
28
+ process.exit(values.help ? 0 : 2);
29
+ }
30
+ const input = positionals[0] === "-" ? readFileSync(0, "utf8") : readFileSync(positionals[0], "utf8");
31
+ const doc = JSON.parse(input);
32
+ const source = values.protocol
33
+ ? await generateProtocolTypes(doc)
34
+ : await generateAgentTypes(doc, {
35
+ typeName: values.name,
36
+ jsonSchemas: values["json-schemas"]
37
+ });
38
+ if (values.output === undefined)
39
+ process.stdout.write(source);
40
+ else
41
+ writeFileSync(values.output, source);
42
+ }
43
+ main().catch((error) => {
44
+ console.error(`harness-codegen: ${error instanceof Error ? error.message : String(error)}`);
45
+ process.exit(1);
46
+ });
@@ -0,0 +1,38 @@
1
+ import { type JSONSchema } from "json-schema-to-typescript";
2
+ /** The document `temporal-agent-harness schema module:Class` prints. */
3
+ export interface AgentSchemaDocument {
4
+ agent: string;
5
+ source: string;
6
+ /** Absent in documents from before init data existed; treated as `null`. */
7
+ init_data?: {
8
+ data: SchemaRef;
9
+ required: boolean;
10
+ } | null;
11
+ handlers: Record<string, {
12
+ description: string;
13
+ mid_turn: string;
14
+ input: SchemaRef;
15
+ output: SchemaRef;
16
+ }>;
17
+ states: Record<string, SchemaRef>;
18
+ $defs: Record<string, JSONSchema>;
19
+ }
20
+ /** The document `temporal-agent-harness schema --protocol` prints. */
21
+ export interface ProtocolSchemaDocument {
22
+ event: SchemaRef;
23
+ stream_items: SchemaRef[];
24
+ $defs: Record<string, JSONSchema>;
25
+ }
26
+ interface SchemaRef {
27
+ $ref: string;
28
+ }
29
+ export interface GenerateOptions {
30
+ /** Name of the mapping type. Defaults to the agent's workflow type name. */
31
+ typeName?: string;
32
+ /** Also emit `<typeName>Schemas`: each handler's input and output as JSON Schema, for
33
+ * validating them at runtime with any validator. */
34
+ jsonSchemas?: boolean;
35
+ }
36
+ export declare function generateAgentTypes(doc: AgentSchemaDocument, options?: GenerateOptions): Promise<string>;
37
+ export declare function generateProtocolTypes(doc: ProtocolSchemaDocument): Promise<string>;
38
+ export {};
@@ -0,0 +1,233 @@
1
+ // TypeScript types from the documents `temporal-agent-harness schema` prints. For one agent:
2
+ // an interface per model in its `$defs`, one mapping type naming its init data, every
3
+ // handler's input and output and every declared state, and a same-named definition value that
4
+ // `startSession` takes. For the event protocol (`--protocol`): an interface
5
+ // per event payload and the envelope, plus the union of payloads and of their `type`s.
6
+ import { compile } from "json-schema-to-typescript";
7
+ const REF_PREFIX = "#/$defs/";
8
+ const ROOT = "HarnessCodegenRoot";
9
+ // Constraints json-schema-to-typescript drops, carried into the JSDoc instead.
10
+ const CONSTRAINT_KEYWORDS = [
11
+ "minimum",
12
+ "maximum",
13
+ "exclusiveMinimum",
14
+ "exclusiveMaximum",
15
+ "minLength",
16
+ "maxLength",
17
+ "pattern",
18
+ "minItems",
19
+ "maxItems"
20
+ ];
21
+ // Keywords whose values are data rather than schemas, so they are copied as they are.
22
+ const DATA_KEYWORDS = new Set(["default", "examples", "const", "enum"]);
23
+ export async function generateAgentTypes(doc, options = {}) {
24
+ const names = typeNames(Object.keys(doc.$defs));
25
+ const typeName = options.typeName ?? safeName(doc.agent);
26
+ for (const name of options.jsonSchemas ? [typeName, `${typeName}Schemas`] : [typeName]) {
27
+ if ([...names.values()].includes(name)) {
28
+ throw new Error(`the generated name ${name} is also the name of one of the agent's models; ` +
29
+ "pass a different type name");
30
+ }
31
+ }
32
+ return [
33
+ `// GENERATED by @temporal-agent-harness/codegen from ${doc.source}. Do not edit.`,
34
+ "",
35
+ await compileModels(doc.$defs, names),
36
+ "",
37
+ mappingType(doc, typeName, names),
38
+ "",
39
+ definitionValue(doc, typeName),
40
+ ...(options.jsonSchemas ? ["", handlerSchemas(doc, typeName)] : []),
41
+ ""
42
+ ].join("\n");
43
+ }
44
+ /** Every handler's input and output as a self-contained JSON Schema: a `$ref` to its model,
45
+ * with `$defs` holding just the models that one reaches. */
46
+ function handlerSchemas(doc, typeName) {
47
+ const standalone = (ref) => {
48
+ const $defs = {};
49
+ const visit = (node) => {
50
+ if (Array.isArray(node))
51
+ return node.forEach(visit);
52
+ if (node === null || typeof node !== "object")
53
+ return;
54
+ for (const [key, value] of Object.entries(node)) {
55
+ if (key !== "$ref" || typeof value !== "string")
56
+ visit(value);
57
+ else if (value.startsWith(REF_PREFIX) && !(value.slice(REF_PREFIX.length) in $defs)) {
58
+ const name = value.slice(REF_PREFIX.length);
59
+ $defs[name] = doc.$defs[name];
60
+ visit($defs[name]);
61
+ }
62
+ }
63
+ };
64
+ visit(ref);
65
+ return { $ref: ref.$ref, $defs };
66
+ };
67
+ const schemas = Object.fromEntries(Object.entries(doc.handlers).map(([name, h]) => [name, { input: standalone(h.input), output: standalone(h.output) }]));
68
+ return [
69
+ `/** Each \`${doc.agent}\` handler's input and output as JSON Schema, to validate them with any`,
70
+ " * JSON Schema validator. */",
71
+ `export const ${typeName}Schemas = ${JSON.stringify(schemas, null, 2)} as const;`
72
+ ].join("\n");
73
+ }
74
+ export async function generateProtocolTypes(doc) {
75
+ const names = typeNames(Object.keys(doc.$defs));
76
+ const resolve = refResolver(names);
77
+ const items = doc.stream_items.map(resolve);
78
+ return [
79
+ "// GENERATED by @temporal-agent-harness/codegen from the harness event protocol",
80
+ "// (`temporal-agent-harness schema --protocol`). Do not edit.",
81
+ "",
82
+ await compileModels(doc.$defs, names),
83
+ "",
84
+ `/** Every payload an \`${resolve(doc.event)}\` can carry, discriminated by \`type\`. */`,
85
+ `export type AgentStreamItem =\n${items.map((name) => ` | ${name}`).join("\n")};`,
86
+ "",
87
+ "/** The `type` of every event on the stream. */",
88
+ 'export type AgentEventType = AgentStreamItem["type"];',
89
+ ""
90
+ ].join("\n");
91
+ }
92
+ async function compileModels($defs, names) {
93
+ const resolve = refResolver(names);
94
+ const defs = {};
95
+ for (const [key, schema] of Object.entries($defs)) {
96
+ defs[key] = { ...prepare(schema, resolve), title: names.get(key) };
97
+ }
98
+ const root = {
99
+ title: ROOT,
100
+ anyOf: Object.keys(defs).map((key) => ({ $ref: `${REF_PREFIX}${key}` })),
101
+ $defs: defs
102
+ };
103
+ const models = await compile(root, ROOT, {
104
+ bannerComment: "",
105
+ additionalProperties: false,
106
+ unknownAny: true,
107
+ strictIndexSignatures: true
108
+ });
109
+ return dropRoot(models).trim();
110
+ }
111
+ /** Shape a model's schema for json-schema-to-typescript, which would otherwise mint extra
112
+ * named types: one per property-level title, and one per `$ref` that carries a sibling
113
+ * keyword (pydantic puts a property's description next to its `$ref`). Titles are dropped
114
+ * and each `$ref` becomes the referenced type's name, keeping the description. Constraints
115
+ * are folded into the description. */
116
+ function prepare(node, resolve) {
117
+ if (Array.isArray(node))
118
+ return node.map((item) => prepare(item, resolve));
119
+ if (node === null || typeof node !== "object")
120
+ return node;
121
+ if (typeof node.$ref === "string") {
122
+ const { description } = node;
123
+ return {
124
+ tsType: resolve(node),
125
+ ...(typeof description === "string" ? { description: fromSphinx(description) } : {})
126
+ };
127
+ }
128
+ const out = {};
129
+ for (const [key, value] of Object.entries(node)) {
130
+ if (key === "title")
131
+ continue;
132
+ if (DATA_KEYWORDS.has(key))
133
+ out[key] = value;
134
+ else if (key === "properties" || key === "$defs" || key === "patternProperties")
135
+ out[key] = Object.fromEntries(Object.entries(value).map(([k, v]) => [k, prepare(v, resolve)]));
136
+ else
137
+ out[key] = prepare(value, resolve);
138
+ }
139
+ if (typeof out.description === "string")
140
+ out.description = fromSphinx(out.description);
141
+ const tags = CONSTRAINT_KEYWORDS.filter((k) => k in out).map((k) => `@${k} ${String(out[k])}`);
142
+ if (tags.length > 0) {
143
+ const description = typeof out.description === "string" ? `${out.description}\n\n` : "";
144
+ out.description = description + tags.join("\n");
145
+ }
146
+ return out;
147
+ }
148
+ /** Python docstrings use reStructuredText roles; JSDoc renders Markdown. */
149
+ function fromSphinx(text) {
150
+ return text
151
+ .replace(/:[a-z]+:`~?([^`]+)`/g, "`$1`")
152
+ .replace(/``([^`]+)``/g, "`$1`");
153
+ }
154
+ function refResolver(names) {
155
+ return (ref) => {
156
+ const key = ref.$ref.startsWith(REF_PREFIX) ? ref.$ref.slice(REF_PREFIX.length) : undefined;
157
+ const name = key === undefined ? undefined : names.get(key);
158
+ if (name === undefined)
159
+ throw new Error(`unresolvable $ref ${ref.$ref}`);
160
+ return name;
161
+ };
162
+ }
163
+ function mappingType(doc, typeName, names) {
164
+ const resolve = refResolver(names);
165
+ const handlers = Object.entries(doc.handlers).map(([name, h]) => `${jsdoc(h.description, " ")} ${name}: { input: ${resolve(h.input)}; output: ${resolve(h.output)} };`);
166
+ const states = Object.entries(doc.states).map(([id, ref]) => ` ${id}: ${resolve(ref)};`);
167
+ const block = (lines) => lines.length === 0 ? "Record<never, never>" : `{\n${lines.join("\n")}\n }`;
168
+ const initData = doc.init_data
169
+ ? `{ data: ${resolve(doc.init_data.data)}; required: ${doc.init_data.required} }`
170
+ : "null";
171
+ return [
172
+ `/** The \`${doc.agent}\` agent's init data, handlers (message in, reply out) and observable`,
173
+ " * state. */",
174
+ `export interface ${typeName} {`,
175
+ ` initData: ${initData};`,
176
+ ` handlers: ${block(handlers)};`,
177
+ ` states: ${block(states)};`,
178
+ "}"
179
+ ].join("\n");
180
+ }
181
+ /** The value `startSession` takes, named like the mapping type (a type and a value may share a
182
+ * name). Its type carries the mapping type as a phantom `schema`, so the file needs no import. */
183
+ function definitionValue(doc, typeName) {
184
+ const workflowType = JSON.stringify(doc.agent);
185
+ return [
186
+ `/** Pass to \`startSession\` to start a \`${doc.agent}\` session. */`,
187
+ `export const ${typeName}: { readonly workflowType: ${workflowType}; readonly schema?: ${typeName} } = {`,
188
+ ` workflowType: ${workflowType}`,
189
+ "};"
190
+ ].join("\n");
191
+ }
192
+ function jsdoc(text, indent) {
193
+ const lines = text.trim().replaceAll("*/", "*\\/").split("\n");
194
+ if (lines.length === 0 || lines[0] === "")
195
+ return "";
196
+ if (lines.length === 1)
197
+ return `${indent}/** ${lines[0]} */\n`;
198
+ return `${indent}/**\n${lines.map((l) => `${indent} * ${l}`.trimEnd()).join("\n")}\n${indent} */\n`;
199
+ }
200
+ /** The generated name for every `$defs` key, rejecting two keys that would share one. */
201
+ function typeNames(keys) {
202
+ const names = new Map();
203
+ const owners = new Map();
204
+ for (const key of keys) {
205
+ const name = safeName(key);
206
+ const other = owners.get(name);
207
+ if (other !== undefined) {
208
+ throw new Error(`models ${other} and ${key} would both be generated as ${name}`);
209
+ }
210
+ owners.set(name, key);
211
+ names.set(key, name);
212
+ }
213
+ return names;
214
+ }
215
+ /** A TypeScript identifier for a schema name: `Echo-Input` → `EchoInput`. Already-safe
216
+ * PascalCase names come back unchanged, which json-schema-to-typescript also leaves alone. */
217
+ function safeName(raw) {
218
+ const name = raw
219
+ .split(/[^A-Za-z0-9]+/)
220
+ .filter(Boolean)
221
+ .map((part) => part[0].toUpperCase() + part.slice(1))
222
+ .join("");
223
+ if (!/^[A-Za-z]/.test(name))
224
+ throw new Error(`cannot derive a type name from ${raw}`);
225
+ return name;
226
+ }
227
+ function dropRoot(source) {
228
+ const start = source.indexOf(`export type ${ROOT} =`);
229
+ if (start === -1)
230
+ return source;
231
+ const end = source.indexOf(";\n", start);
232
+ return source.slice(0, start) + source.slice(end + 2);
233
+ }
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@temporalio/agent-harness-codegen",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "Generate TypeScript types for a temporal-agent-harness agent from its `temporal-agent-harness schema` document.",
6
+ "bin": {
7
+ "harness-codegen": "dist/src/cli.js"
8
+ },
9
+ "dependencies": {
10
+ "json-schema-to-typescript": "^16.0.0"
11
+ },
12
+ "devDependencies": {
13
+ "@types/node": "^22.20.4",
14
+ "typescript": "^7.0.2"
15
+ },
16
+ "license": "MIT",
17
+ "files": [
18
+ "dist/src"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "registry": "https://registry.npmjs.org/"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/temporal-community/temporal-agent-harness.git",
27
+ "directory": "typescript/packages/codegen"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json --outDir dist",
31
+ "typecheck": "tsc -p tsconfig.json --noEmit",
32
+ "test": "tsc -p tsconfig.json --outDir dist && node --test dist/test/*.test.js"
33
+ }
6
34
  }