@spunto/build 0.22.0 → 0.23.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 +26 -0
- package/package.json +9 -2
- package/src/agent-contract/agent-contract.v1.schema.json +672 -0
- package/src/agent-contract/conformance.ts +315 -0
- package/src/agent-contract/examples/fake-agent.mjs +46 -0
- package/src/agent-contract/index.ts +20 -0
- package/src/agent-contract/vocabulary.ts +313 -0
- package/src/agent-runners/claude/adapter.ts +330 -0
- package/src/agent-runners/claude.mjs +40 -17
- package/src/agent-runners/codex/adapter.ts +135 -0
- package/src/agent-runners/codex.mjs +15 -7
- package/src/agent-runners/index.ts +23 -14
- package/src/agent-stream/agent-stream.ts +45 -435
- package/src/agent-stream/draft.ts +41 -2
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// The Claude runner (RFC 0048):
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// The Claude runner (RFC 0048): written into the machine by the platform when a session starts —
|
|
3
|
+
// one file, **bundled** with its adapter by the platform (esbuild) so there is nothing to install —
|
|
4
|
+
// and run with the Node the `spunto-agent-claude` feature put in the image. It drives Claude
|
|
5
|
+
// through the official Agent SDK the same feature installed.
|
|
5
6
|
//
|
|
6
|
-
// The contract, the same for every agent app (`../agent-
|
|
7
|
+
// The Spunto agent contract, the same for every agent app (docs/agent-contract.md, `../agent-contract`):
|
|
7
8
|
//
|
|
8
9
|
// node claude.mjs run [--model <id>] [--resume <session id>]
|
|
9
10
|
// stdin one JSON object per line — {"type":"user","text"} · {"type":"interrupt","id"} ·
|
|
10
11
|
// {"type":"title","id","description"} ; closed = the session ends after its turn
|
|
11
|
-
// stdout
|
|
12
|
-
//
|
|
13
|
-
//
|
|
12
|
+
// stdout **our** vocabulary, one JSON object per line (`agent-v1`): each SDK message is read
|
|
13
|
+
// here (`./claude/adapter.ts`) and printed as the events it means, with the message
|
|
14
|
+
// itself as `source` — the platform has no Claude reader for it
|
|
14
15
|
// env SPUNTO_AGENT_OPTIONS — the task's parameters, as declared by the app's catalog
|
|
15
16
|
// (`./catalogs.ts`): `{"effort":"high"}`
|
|
16
17
|
// SPUNTO_AGENT_MCP — the org's MCP servers (RFC 0049), in the platform's words:
|
|
@@ -21,10 +22,14 @@
|
|
|
21
22
|
// exit 0 once the input is closed and the last turn is over, 1 when the session could not run
|
|
22
23
|
//
|
|
23
24
|
// Credentials are not this file's business: whatever the member set as secrets (an
|
|
24
|
-
// `ANTHROPIC_API_KEY`) reaches the SDK's binary through the environment, untouched
|
|
25
|
-
//
|
|
25
|
+
// `ANTHROPIC_API_KEY`) reaches the SDK's binary through the environment, untouched. The one thing
|
|
26
|
+
// the runner does with that key itself is ask the Models API how large the session's window is
|
|
27
|
+
// (`resolveContextWindow`) — an API key only, never a subscription token, which is the rule the
|
|
28
|
+
// platform followed when it asked on the session's behalf.
|
|
26
29
|
|
|
27
30
|
import { createInterface } from "node:readline"
|
|
31
|
+
import { claudeTranslator } from "./claude/adapter"
|
|
32
|
+
import { resolveContextWindow } from "../agent-stream/model-window"
|
|
28
33
|
|
|
29
34
|
const SDK = process.env.SPUNTO_CLAUDE_SDK || "/opt/spunto/agents/claude/node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs"
|
|
30
35
|
|
|
@@ -32,12 +37,32 @@ function out(obj) {
|
|
|
32
37
|
process.stdout.write(`${JSON.stringify(obj)}\n`)
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
/** A failure the platform can read: the
|
|
40
|
+
/** A failure the platform can read: the turn ends, failed, saying why — then a non-zero exit. */
|
|
36
41
|
function fail(message) {
|
|
37
|
-
out({ type: "
|
|
42
|
+
out({ type: "session.ended", status: "failed", stopReason: null, summary: message })
|
|
38
43
|
process.exit(1)
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
/** How the window is asked about: an API key, the only credential the platform ever used for it. */
|
|
47
|
+
const apiKey = process.env.ANTHROPIC_API_KEY?.trim()
|
|
48
|
+
const credential = apiKey ? { kind: "api-key", value: apiKey } : null
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Print one SDK message as the contract. The line that opens the session carries the model's own
|
|
52
|
+
* window when the Models API answers (the adapter only knows a table), so it is measured here, once
|
|
53
|
+
* per session, before the line goes out — the platform no longer asks anyone on a session's behalf.
|
|
54
|
+
*/
|
|
55
|
+
const translate = claudeTranslator()
|
|
56
|
+
async function emit(message) {
|
|
57
|
+
for (const line of translate(message)) {
|
|
58
|
+
if (line.type === "session.started" && typeof line.model === "string") {
|
|
59
|
+
const measured = await resolveContextWindow(line.model, credential).catch(() => null)
|
|
60
|
+
if (measured !== null) line.contextWindow = measured
|
|
61
|
+
}
|
|
62
|
+
out(line)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
function parseArgs(argv) {
|
|
42
67
|
const [verb, ...rest] = argv
|
|
43
68
|
if (verb !== "run") fail(`claude runner: unknown verb "${verb ?? ""}" (expected "run")`)
|
|
@@ -165,15 +190,13 @@ createInterface({ input: process.stdin, crlfDelay: Infinity })
|
|
|
165
190
|
} else if (msg.type === "interrupt") {
|
|
166
191
|
session.interrupt().catch((err) => process.stderr.write(`claude runner: interrupt failed: ${err?.message ?? err}\n`))
|
|
167
192
|
} else if (msg.type === "title" && typeof msg.description === "string") {
|
|
168
|
-
//
|
|
169
|
-
// a title one way whichever side of the SDK the session runs on.
|
|
193
|
+
// Asked of the SDK (Claude names the conversation from the human's words), and said in band.
|
|
170
194
|
if (typeof session.generateSessionTitle !== "function") return
|
|
171
195
|
session
|
|
172
196
|
.generateSessionTitle(msg.description, { persist: true })
|
|
173
197
|
.then((title) => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
198
|
+
const clean = typeof title === "string" ? title.replace(/\s+/g, " ").trim().slice(0, 200) : ""
|
|
199
|
+
if (clean) out({ type: "session.title", title: clean })
|
|
177
200
|
})
|
|
178
201
|
.catch((err) => process.stderr.write(`claude runner: title failed: ${err?.message ?? err}\n`))
|
|
179
202
|
}
|
|
@@ -185,7 +208,7 @@ createInterface({ input: process.stdin, crlfDelay: Infinity })
|
|
|
185
208
|
let lastType = null
|
|
186
209
|
try {
|
|
187
210
|
for await (const message of session) {
|
|
188
|
-
|
|
211
|
+
await emit(message)
|
|
189
212
|
lastType = message?.type ?? null
|
|
190
213
|
}
|
|
191
214
|
} catch (err) {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex's dialect, read into our vocabulary — the events of `@openai/codex-sdk`'s `runStreamed()`
|
|
3
|
+
* (`ThreadEvent`). Written against the SDK's own types (0.164.0).
|
|
4
|
+
*
|
|
5
|
+
* Lives with the Codex runner (RFC 0048 § 9): the runner bundles it and prints the contract
|
|
6
|
+
* (`codexContractLines`). The platform's legacy reader still imports `adaptCodex` for sessions
|
|
7
|
+
* started on the runner's old dialect, until they end.
|
|
8
|
+
*
|
|
9
|
+
* Codex works per **turn**, not per session: `turn.completed` is a hand-back the way Claude's
|
|
10
|
+
* `result` is, so it ends the turn here too. Its usage is the turn's sum, which is why it is filed
|
|
11
|
+
* as a `call` (summed over the task) without a `contextTokens`: no single number of it is the
|
|
12
|
+
* size of the prompt last sent.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { clip, type NormalizedEvent } from "../../agent-contract/vocabulary"
|
|
16
|
+
|
|
17
|
+
function num(value: unknown): number | null {
|
|
18
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function adaptCodex(obj: Record<string, unknown>): NormalizedEvent[] {
|
|
22
|
+
const type = obj.type
|
|
23
|
+
if (type === "thread.started") {
|
|
24
|
+
return [{ type: "session.started", payload: { harness: "codex", model: null, sessionId: typeof obj.thread_id === "string" ? obj.thread_id : null } }]
|
|
25
|
+
}
|
|
26
|
+
if (type === "turn.started") return []
|
|
27
|
+
if (type === "turn.completed") {
|
|
28
|
+
const u = (obj.usage ?? {}) as Record<string, unknown>
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
type: "usage",
|
|
32
|
+
payload: {
|
|
33
|
+
scope: "call",
|
|
34
|
+
model: null,
|
|
35
|
+
inputTokens: num(u.input_tokens) ?? 0,
|
|
36
|
+
outputTokens: (num(u.output_tokens) ?? 0) + (num(u.reasoning_output_tokens) ?? 0),
|
|
37
|
+
cacheReadTokens: num(u.cached_input_tokens) ?? 0,
|
|
38
|
+
cacheCreationTokens: num(u.cache_write_input_tokens) ?? 0,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{ type: "session.ended", payload: { status: "succeeded", stopReason: null, summary: "" } },
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
if (type === "turn.failed") {
|
|
45
|
+
const error = (obj.error ?? {}) as Record<string, unknown>
|
|
46
|
+
return [{ type: "session.ended", payload: { status: "failed", stopReason: null, summary: clip(error.message).text } }]
|
|
47
|
+
}
|
|
48
|
+
// Not an end: measured, Codex emits `error` for every retry ("Reconnecting... 2/5 (401 …)") and
|
|
49
|
+
// only the `turn.failed` that follows the last one ends the turn. Kept visible, not fatal.
|
|
50
|
+
if (type === "error") return [{ type: "raw", payload: { json: obj } }]
|
|
51
|
+
if (type !== "item.started" && type !== "item.updated" && type !== "item.completed") {
|
|
52
|
+
return [{ type: "raw", payload: { json: obj } }]
|
|
53
|
+
}
|
|
54
|
+
const item = (obj.item ?? {}) as Record<string, unknown>
|
|
55
|
+
const callId = typeof item.id === "string" ? item.id : null
|
|
56
|
+
const started = type === "item.started"
|
|
57
|
+
const completed = type === "item.completed"
|
|
58
|
+
switch (item.type) {
|
|
59
|
+
case "agent_message": {
|
|
60
|
+
if (!completed) return []
|
|
61
|
+
const { text, truncated } = clip(item.text)
|
|
62
|
+
return text ? [{ type: "message", payload: { role: "assistant", text, truncated } }] : []
|
|
63
|
+
}
|
|
64
|
+
case "reasoning": {
|
|
65
|
+
if (!completed) return []
|
|
66
|
+
const { text, truncated } = clip(item.text)
|
|
67
|
+
return text ? [{ type: "thinking", payload: { text, truncated } }] : []
|
|
68
|
+
}
|
|
69
|
+
case "command_execution": {
|
|
70
|
+
if (started) {
|
|
71
|
+
const { text: input, truncated } = clip({ command: item.command })
|
|
72
|
+
return [{ type: "tool.call", payload: { callId, name: "Bash", input, truncated } }]
|
|
73
|
+
}
|
|
74
|
+
if (!completed) return []
|
|
75
|
+
const { text: output, truncated } = clip(item.aggregated_output)
|
|
76
|
+
const failed = item.status === "failed" || (typeof item.exit_code === "number" && item.exit_code !== 0)
|
|
77
|
+
return [{ type: "tool.result", payload: { callId, isError: failed, output, truncated } }]
|
|
78
|
+
}
|
|
79
|
+
case "file_change": {
|
|
80
|
+
// Reported once, applied: the call and its outcome arrive together.
|
|
81
|
+
if (!completed) return []
|
|
82
|
+
const changes = Array.isArray(item.changes) ? item.changes : []
|
|
83
|
+
const { text: input, truncated } = clip({ changes })
|
|
84
|
+
const output = changes
|
|
85
|
+
.filter((c): c is Record<string, unknown> => !!c && typeof c === "object")
|
|
86
|
+
.map((c) => `${c.kind} ${c.path}`)
|
|
87
|
+
.join("\n")
|
|
88
|
+
return [
|
|
89
|
+
{ type: "tool.call", payload: { callId, name: "Edit", input, truncated } },
|
|
90
|
+
{ type: "tool.result", payload: { callId, isError: item.status === "failed", output, truncated: false } },
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
case "mcp_tool_call": {
|
|
94
|
+
const name = `${typeof item.server === "string" ? item.server : "mcp"}.${typeof item.tool === "string" ? item.tool : "tool"}`
|
|
95
|
+
if (started) {
|
|
96
|
+
const { text: input, truncated } = clip(item.arguments)
|
|
97
|
+
return [{ type: "tool.call", payload: { callId, name, input, truncated } }]
|
|
98
|
+
}
|
|
99
|
+
if (!completed) return []
|
|
100
|
+
const error = (item.error ?? null) as Record<string, unknown> | null
|
|
101
|
+
const { text: output, truncated } = clip(error ? error.message : (item.result as Record<string, unknown> | undefined)?.content ?? "")
|
|
102
|
+
return [{ type: "tool.result", payload: { callId, isError: !!error || item.status === "failed", output, truncated } }]
|
|
103
|
+
}
|
|
104
|
+
case "web_search": {
|
|
105
|
+
if (!started) return []
|
|
106
|
+
const { text: input, truncated } = clip({ query: item.query })
|
|
107
|
+
return [{ type: "tool.call", payload: { callId, name: "WebSearch", input, truncated } }]
|
|
108
|
+
}
|
|
109
|
+
case "todo_list": {
|
|
110
|
+
const steps = (Array.isArray(item.items) ? item.items : [])
|
|
111
|
+
.filter((t): t is Record<string, unknown> => !!t && typeof t === "object")
|
|
112
|
+
.map((t) => ({ text: clip(t.text).text, status: t.completed === true ? "completed" : "pending" }))
|
|
113
|
+
.filter((s) => s.text)
|
|
114
|
+
return steps.length > 0 ? [{ type: "plan", payload: { steps } }] : []
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [{ type: "raw", payload: { json: obj } }]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* An `agent_message` item being written, as the contract's transient `message.delta`. Codex hands
|
|
122
|
+
* the whole text so far on `item.started` / `item.updated`, not a delta, so every one opens the
|
|
123
|
+
* draft afresh with it (`start`); the completed item, stored as the `message`, closes it.
|
|
124
|
+
*/
|
|
125
|
+
export function codexDelta(obj: Record<string, unknown>): Record<string, unknown>[] {
|
|
126
|
+
if (obj.type !== "item.started" && obj.type !== "item.updated") return []
|
|
127
|
+
const item = (obj.item ?? {}) as Record<string, unknown>
|
|
128
|
+
if (item.type !== "agent_message" || typeof item.text !== "string" || !item.text) return []
|
|
129
|
+
return [{ type: "message.delta", start: true, text: item.text }]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** One `ThreadEvent`, as the lines of the contract the Codex runner prints, each with the event as `source`. */
|
|
133
|
+
export function codexContractLines(event: Record<string, unknown>): Record<string, unknown>[] {
|
|
134
|
+
return [...codexDelta(event), ...adaptCodex(event).map((e) => ({ type: e.type, ...e.payload, source: event }))]
|
|
135
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The Codex runner (RFC 0048): same contract as `claude.mjs`, over `@openai/codex-sdk`, installed
|
|
3
|
-
// with the `codex` binary it drives by the `spunto-agent-codex` feature.
|
|
3
|
+
// with the `codex` binary it drives by the `spunto-agent-codex` feature. Bundled with its adapter
|
|
4
|
+
// by the platform, like every runner.
|
|
4
5
|
//
|
|
5
6
|
// node codex.mjs run [--model <id>] [--resume <thread id>]
|
|
6
7
|
// stdin {"type":"user","text"} · {"type":"interrupt","id"} · {"type":"title",…} (ignored: Codex
|
|
7
8
|
// does not name its threads) ; closed = the session ends after its turn
|
|
8
|
-
// stdout
|
|
9
|
+
// stdout **our** vocabulary (`agent-v1`): each `ThreadEvent` read here (`./codex/adapter.ts`),
|
|
10
|
+
// with the event itself as `source`
|
|
9
11
|
// env SPUNTO_AGENT_MCP — the org's MCP servers, through the platform's gateway (RFC 0049)
|
|
10
12
|
// env SPUNTO_AGENT_OPTIONS — the task's parameters (`./catalogs.ts`):
|
|
11
13
|
// `{"reasoningEffort":"high","webSearch":"live"}`
|
|
@@ -19,6 +21,7 @@
|
|
|
19
21
|
// the machine's terminal (`~/.codex/auth.json`), which the binary reads on its own.
|
|
20
22
|
|
|
21
23
|
import { createInterface } from "node:readline"
|
|
24
|
+
import { codexContractLines } from "./codex/adapter"
|
|
22
25
|
|
|
23
26
|
const SDK = process.env.SPUNTO_CODEX_SDK || "/opt/spunto/agents/codex/node_modules/@openai/codex-sdk/dist/index.js"
|
|
24
27
|
|
|
@@ -26,8 +29,13 @@ function out(obj) {
|
|
|
26
29
|
process.stdout.write(`${JSON.stringify(obj)}\n`)
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
/** One `ThreadEvent` (or one the runner stands in for the SDK), as the contract. */
|
|
33
|
+
function emit(event) {
|
|
34
|
+
for (const line of codexContractLines(event)) out(line)
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
function fail(message) {
|
|
30
|
-
|
|
38
|
+
emit({ type: "turn.failed", error: { message } })
|
|
31
39
|
process.exit(1)
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -110,7 +118,7 @@ const threadOptions = {
|
|
|
110
118
|
const thread = args.resume ? codex.resumeThread(args.resume, threadOptions) : codex.startThread(threadOptions)
|
|
111
119
|
// A resumed thread is not announced by the SDK (`thread.started` only comes with a new one), and
|
|
112
120
|
// the platform learns the session id from that line — so the runner says it.
|
|
113
|
-
if (args.resume)
|
|
121
|
+
if (args.resume) emit({ type: "thread.started", thread_id: args.resume })
|
|
114
122
|
|
|
115
123
|
const pending = []
|
|
116
124
|
let closed = false
|
|
@@ -154,14 +162,14 @@ for (;;) {
|
|
|
154
162
|
try {
|
|
155
163
|
const { events } = await thread.runStreamed(text, { signal: current.signal })
|
|
156
164
|
for await (const event of events) {
|
|
157
|
-
|
|
165
|
+
emit(event)
|
|
158
166
|
lastType = event?.type ?? null
|
|
159
167
|
}
|
|
160
168
|
} catch (err) {
|
|
161
169
|
if (current.signal.aborted) {
|
|
162
|
-
|
|
170
|
+
emit({ type: "turn.failed", error: { message: "Interrupted." } })
|
|
163
171
|
} else if (lastType !== "turn.failed") {
|
|
164
|
-
|
|
172
|
+
emit({ type: "turn.failed", error: { message: `codex runner: ${err?.message ?? err}` } })
|
|
165
173
|
failed = true
|
|
166
174
|
}
|
|
167
175
|
}
|
|
@@ -7,23 +7,25 @@
|
|
|
7
7
|
* installs a private Node and the harness's **official SDK** (with the binary the SDK drives)
|
|
8
8
|
* under `/opt/spunto/agents/<id>`. Shipped by the platform itself (`localScript`), so an app
|
|
9
9
|
* never waits on a registry release;
|
|
10
|
-
* - a **runner**,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* - a **runner**, a `.mjs` entry of ours (`claude.mjs`, `codex.mjs`) with its harness's adapter
|
|
11
|
+
* next to it (`claude/adapter.ts`, `codex/adapter.ts`). The caller **bundles** the two into one
|
|
12
|
+
* file (esbuild, `platform: node`, `format: esm`) and writes it into the machine at the moment a
|
|
13
|
+
* session starts. Not baked: a runner fix reaches the next session, not the next rebuild;
|
|
14
|
+
* - the **Spunto agent contract** (`agent-v1`, `../agent-contract`), which is how the platform
|
|
15
|
+
* writes the runner's stdin and reads its stdout — the same for every app.
|
|
15
16
|
*
|
|
16
|
-
* The runner contract, identical for every app:
|
|
17
|
+
* The runner contract, identical for every app (docs/agent-contract.md):
|
|
17
18
|
*
|
|
18
19
|
* <node> <runner>.mjs run [--model <id>] [--resume <session id>]
|
|
19
20
|
* stdin one JSON object per line: {"type":"user","text"} · {"type":"interrupt","id"} ·
|
|
20
21
|
* {"type":"title","id","description"} (`RunnerInput`); closed ⇒ end after the turn
|
|
21
22
|
* env SPUNTO_AGENT_OPTIONS — the task's parameters (`./catalogs.ts`), one JSON object
|
|
22
|
-
* stdout
|
|
23
|
-
* exit 0 = the input closed and the last turn ended;
|
|
23
|
+
* stdout our vocabulary, one JSON object per line, each event with its harness message as `source`
|
|
24
|
+
* exit 0 = the input closed and the last turn ended; ≠ 0 = the session could not run, and its
|
|
25
|
+
* last line is a failed `session.ended` saying why
|
|
24
26
|
*
|
|
25
|
-
* Pure like the rest of the package: what a caller gets is *where* a runner
|
|
26
|
-
* that starts it.
|
|
27
|
+
* Pure like the rest of the package: what a caller gets is *where* a runner entry is, and the shell
|
|
28
|
+
* that starts it. Bundling and reading the file are the caller's I/O.
|
|
27
29
|
*/
|
|
28
30
|
|
|
29
31
|
import type { AgentProtocol } from "../agent-stream/agent-stream"
|
|
@@ -39,9 +41,14 @@ export type AgentHarness = {
|
|
|
39
41
|
id: AgentHarnessId
|
|
40
42
|
/** The catalog id of the feature the platform injects (`AVAILABLE_FEATURES`, `localScript`). */
|
|
41
43
|
featureId: string
|
|
42
|
-
/** How the platform talks to the runner. */
|
|
44
|
+
/** How the platform talks to the runner — the contract, for every app. */
|
|
43
45
|
protocol: AgentProtocol
|
|
44
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* What this app's runner printed before the contract, and so how a session it started then is
|
|
48
|
+
* still read, until it ends (RFC 0048 § 9: a session keeps the dialect it began on).
|
|
49
|
+
*/
|
|
50
|
+
legacyProtocol: AgentProtocol
|
|
51
|
+
/** The runner's entry, a file of this package — to be bundled with what it imports. */
|
|
45
52
|
runnerFile: URL
|
|
46
53
|
/** Where the harness looks for user-level skills (RFC 0033), as shell evaluated in the machine. */
|
|
47
54
|
skillDirs: string[]
|
|
@@ -51,14 +58,16 @@ export const AGENT_HARNESSES: Record<AgentHarnessId, AgentHarness> = {
|
|
|
51
58
|
claude: {
|
|
52
59
|
id: "claude",
|
|
53
60
|
featureId: "spunto-agent-claude",
|
|
54
|
-
protocol: "
|
|
61
|
+
protocol: "agent-v1",
|
|
62
|
+
legacyProtocol: "claude-runner",
|
|
55
63
|
runnerFile: new URL("./claude.mjs", import.meta.url),
|
|
56
64
|
skillDirs: ["${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills"],
|
|
57
65
|
},
|
|
58
66
|
codex: {
|
|
59
67
|
id: "codex",
|
|
60
68
|
featureId: "spunto-agent-codex",
|
|
61
|
-
protocol: "
|
|
69
|
+
protocol: "agent-v1",
|
|
70
|
+
legacyProtocol: "codex-runner",
|
|
62
71
|
runnerFile: new URL("./codex.mjs", import.meta.url),
|
|
63
72
|
skillDirs: ["${CODEX_HOME:-$HOME/.codex}/skills"],
|
|
64
73
|
},
|