@united-workforce/cli 0.6.1 → 0.8.1
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 +120 -5
- package/dist/.build-fingerprint +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.js +9 -2
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -1
- package/dist/__tests__/broker-prompt.test.d.ts +10 -0
- package/dist/__tests__/broker-prompt.test.d.ts.map +1 -0
- package/dist/__tests__/broker-prompt.test.js +129 -0
- package/dist/__tests__/broker-prompt.test.js.map +1 -0
- package/dist/__tests__/broker-step-active-turns.test.d.ts +20 -0
- package/dist/__tests__/broker-step-active-turns.test.d.ts.map +1 -0
- package/dist/__tests__/broker-step-active-turns.test.js +428 -0
- package/dist/__tests__/broker-step-active-turns.test.js.map +1 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts +13 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts.map +1 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.js +429 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.js.map +1 -0
- package/dist/__tests__/config.test.js +33 -37
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/e2e-broker-step-suspend.test.d.ts +18 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.js +313 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.js.map +1 -0
- package/dist/__tests__/e2e-broker-step.test.d.ts +13 -0
- package/dist/__tests__/e2e-broker-step.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-broker-step.test.js +278 -0
- package/dist/__tests__/e2e-broker-step.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts +28 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js +322 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js.map +1 -0
- package/dist/__tests__/log-tag-validity.test.d.ts +2 -0
- package/dist/__tests__/log-tag-validity.test.d.ts.map +1 -0
- package/dist/__tests__/log-tag-validity.test.js +110 -0
- package/dist/__tests__/log-tag-validity.test.js.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.js +35 -23
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.js +5 -2
- package/dist/__tests__/setup-no-llm.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.js +9 -6
- package/dist/__tests__/step-ask.test.js.map +1 -1
- package/dist/__tests__/step-show-json.test.js +5 -5
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-show-text.test.d.ts +2 -0
- package/dist/__tests__/step-show-text.test.d.ts.map +1 -0
- package/dist/__tests__/step-show-text.test.js +192 -0
- package/dist/__tests__/step-show-text.test.js.map +1 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.d.ts +21 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.js +356 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.js.map +1 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.d.ts +21 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.js +476 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.js.map +1 -0
- package/dist/__tests__/step-turns.test.d.ts +24 -0
- package/dist/__tests__/step-turns.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns.test.js +646 -0
- package/dist/__tests__/step-turns.test.js.map +1 -0
- package/dist/__tests__/store-turn-chain.test.d.ts +2 -0
- package/dist/__tests__/store-turn-chain.test.d.ts.map +1 -0
- package/dist/__tests__/store-turn-chain.test.js +341 -0
- package/dist/__tests__/store-turn-chain.test.js.map +1 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.js +3 -3
- package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -1
- package/dist/__tests__/thread-list-limit-offset.test.d.ts +24 -0
- package/dist/__tests__/thread-list-limit-offset.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-limit-offset.test.js +254 -0
- package/dist/__tests__/thread-list-limit-offset.test.js.map +1 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js +7 -2
- package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -1
- package/dist/__tests__/thread-poke.test.js +6 -6
- package/dist/__tests__/thread-poke.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +2 -2
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-suspend-step.test.js +1 -1
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread.test.js +28 -14
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/cli.js +910 -344
- package/dist/cli.js.map +1 -1
- package/dist/commands/broker-step.d.ts +117 -0
- package/dist/commands/broker-step.d.ts.map +1 -0
- package/dist/commands/broker-step.js +654 -0
- package/dist/commands/broker-step.js.map +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +2 -23
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +43 -51
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +6 -4
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +24 -27
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +54 -6
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +484 -134
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +4 -0
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +77 -151
- package/dist/commands/thread.js.map +1 -1
- package/dist/output-mappers.d.ts +8 -0
- package/dist/output-mappers.d.ts.map +1 -1
- package/dist/output-mappers.js +72 -18
- package/dist/output-mappers.js.map +1 -1
- package/dist/schemas.d.ts +3 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +17 -3
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +147 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +254 -1
- package/dist/store.js.map +1 -1
- package/dist/text-renderers.d.ts.map +1 -1
- package/dist/text-renderers.js +27 -2
- package/dist/text-renderers.js.map +1 -1
- package/package.json +7 -5
- package/src/__tests__/agent-resolution-llm-free.test.ts +14 -2
- package/src/__tests__/broker-prompt.test.ts +142 -0
- package/src/__tests__/broker-step-active-turns.test.ts +509 -0
- package/src/__tests__/broker-step-turn-chain-phase2.test.ts +525 -0
- package/src/__tests__/config.test.ts +35 -39
- package/src/__tests__/e2e-broker-step-suspend.test.ts +351 -0
- package/src/__tests__/e2e-broker-step.test.ts +320 -0
- package/src/__tests__/e2e-mock-agent.test.ts +1 -1
- package/src/__tests__/e2e-thread-resume-timeout-suspend.test.ts +360 -0
- package/src/__tests__/log-tag-validity.test.ts +124 -0
- package/src/__tests__/setup-agent-discovery.test.ts +35 -23
- package/src/__tests__/setup-no-llm.test.ts +5 -2
- package/src/__tests__/step-ask.test.ts +9 -6
- package/src/__tests__/step-show-json.test.ts +5 -5
- package/src/__tests__/step-show-text.test.ts +236 -0
- package/src/__tests__/step-turns-cli-subprocess.test.ts +411 -0
- package/src/__tests__/step-turns-panorama-phase3.test.ts +579 -0
- package/src/__tests__/step-turns.test.ts +734 -0
- package/src/__tests__/store-turn-chain.test.ts +386 -0
- package/src/__tests__/thread-agent-failure-suspended.test.ts +3 -3
- package/src/__tests__/thread-list-limit-offset.test.ts +305 -0
- package/src/__tests__/thread-list-template-ms-date.test.ts +7 -2
- package/src/__tests__/thread-poke.test.ts +6 -6
- package/src/__tests__/thread-resume.test.ts +2 -2
- package/src/__tests__/thread-suspend-step.test.ts +1 -1
- package/src/__tests__/thread.test.ts +29 -15
- package/src/cli.ts +1056 -483
- package/src/commands/broker-step.ts +913 -0
- package/src/commands/config.ts +2 -24
- package/src/commands/prompt.ts +43 -51
- package/src/commands/setup.ts +25 -29
- package/src/commands/step.ts +645 -176
- package/src/commands/thread.ts +87 -192
- package/src/output-mappers.ts +99 -21
- package/src/schemas.ts +32 -2
- package/src/store.ts +297 -2
- package/src/text-renderers.ts +35 -2
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/adapter-json-roundtrip.test.js +0 -160
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +0 -1
- package/dist/__tests__/spawn-agent-json.test.d.ts +0 -2
- package/dist/__tests__/spawn-agent-json.test.d.ts.map +0 -1
- package/dist/__tests__/spawn-agent-json.test.js +0 -79
- package/dist/__tests__/spawn-agent-json.test.js.map +0 -1
- package/src/__tests__/adapter-json-roundtrip.test.ts +0 -193
- package/src/__tests__/spawn-agent-json.test.ts +0 -100
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broker-driven step execution. Replaces the legacy `spawnAgent` /
|
|
3
|
+
* `executeAgentCommand` / last-stdout-line JSON parsing path with
|
|
4
|
+
* `broker.send()` over the Sumeru HTTP API.
|
|
5
|
+
*
|
|
6
|
+
* Phase 3 (#380) — `cmdThreadStepOnce`, `cmdThreadResume`, and `cmdThreadPoke`
|
|
7
|
+
* use this module instead of spawning per-role CLI binaries.
|
|
8
|
+
*/
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { putSchema, validate } from "@ocas/core";
|
|
11
|
+
import { createBroker, createSessionStore, } from "@united-workforce/broker";
|
|
12
|
+
import { SUSPEND_STATUS, } from "@united-workforce/protocol";
|
|
13
|
+
import { createLogger } from "@united-workforce/util";
|
|
14
|
+
import { buildContinuationPrompt, buildFrontmatterRetryPrompt, buildOutputFormatInstruction, buildRolePrompt, buildThreadProgress, mergeUsage, tryFrontmatterFastPath, trySuspendFastPath, } from "@united-workforce/util-agent";
|
|
15
|
+
import { clearActiveStep, clearActiveTurns, getActiveTurnHead, setActiveStep, setActiveTurnHead, writeStepStart, writeTurnNode, } from "../store.js";
|
|
16
|
+
import { expandOutput, fail } from "./shared.js";
|
|
17
|
+
const log = createLogger({ sink: { kind: "stderr" } });
|
|
18
|
+
/** Tag for broker.send call site. */
|
|
19
|
+
const PL_BROKER_SEND = "BR0KR5ND";
|
|
20
|
+
/** Tag for frontmatter retry call sites. */
|
|
21
|
+
const PL_FRONTMATTER_RETRY = "F4RTM4RT";
|
|
22
|
+
/** Tag for frontmatter extraction failure. */
|
|
23
|
+
const PL_FRONTMATTER_FAIL = "F4FA117Z";
|
|
24
|
+
const MAX_FRONTMATTER_RETRIES = 2;
|
|
25
|
+
const DETAIL_SCHEMA = {
|
|
26
|
+
title: "broker-detail",
|
|
27
|
+
type: "object",
|
|
28
|
+
required: ["sessionId", "duration", "turnCount"],
|
|
29
|
+
properties: {
|
|
30
|
+
sessionId: { type: "string" },
|
|
31
|
+
duration: { type: "integer" },
|
|
32
|
+
turnCount: { type: "integer" },
|
|
33
|
+
// Suspend diagnostics (issue #435) — present only on a timeout-suspended
|
|
34
|
+
// step. Optional, so the completed-path detail node is byte-for-byte
|
|
35
|
+
// unchanged (same content hash).
|
|
36
|
+
nativeId: { type: "string" },
|
|
37
|
+
elapsedMs: { type: "integer" },
|
|
38
|
+
reason: { type: "string" },
|
|
39
|
+
},
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Parse `--agent` overrides under the new `{host, gateway}` shape.
|
|
44
|
+
*
|
|
45
|
+
* Accepts:
|
|
46
|
+
* - alias e.g. `hermes` → `config.agents.hermes`
|
|
47
|
+
* - inline e.g. `http://h:7900 gw` → `{host: "http://h:7900", gateway: "gw"}`
|
|
48
|
+
*
|
|
49
|
+
* Single-token forms that don't match an alias fail with the documented
|
|
50
|
+
* message; this fully replaces the legacy "treat anything as a binary path"
|
|
51
|
+
* behaviour.
|
|
52
|
+
*/
|
|
53
|
+
export function parseAgentOverride(override) {
|
|
54
|
+
const trimmed = override.trim();
|
|
55
|
+
if (trimmed === "") {
|
|
56
|
+
fail("agent override must not be empty");
|
|
57
|
+
}
|
|
58
|
+
const parts = trimmed.split(/\s+/).filter((p) => p.length > 0);
|
|
59
|
+
if (parts.length !== 2) {
|
|
60
|
+
fail(`agent override must be an alias or "<host> <gateway>"`);
|
|
61
|
+
}
|
|
62
|
+
const host = parts[0];
|
|
63
|
+
const gateway = parts[1];
|
|
64
|
+
if (host === undefined || gateway === undefined) {
|
|
65
|
+
fail(`agent override must be an alias or "<host> <gateway>"`);
|
|
66
|
+
}
|
|
67
|
+
return { host, gateway };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the agent route for a (workflow, role, override) triple.
|
|
71
|
+
* Mirrors the legacy `resolveAgentConfig` precedence:
|
|
72
|
+
* --agent override > agentOverrides[workflow][role] > defaultAgent
|
|
73
|
+
* Override may be an alias or an inline `"<host> <gateway>"` form.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveAgentRoute(config, workflow, role, agentOverride, cwd) {
|
|
76
|
+
if (agentOverride !== null) {
|
|
77
|
+
const fromAlias = config.agents[agentOverride];
|
|
78
|
+
if (fromAlias !== undefined) {
|
|
79
|
+
return { host: fromAlias.host, gateway: fromAlias.gateway, cwd };
|
|
80
|
+
}
|
|
81
|
+
const parsed = parseAgentOverride(agentOverride);
|
|
82
|
+
return { host: parsed.host, gateway: parsed.gateway, cwd };
|
|
83
|
+
}
|
|
84
|
+
let alias = config.defaultAgent;
|
|
85
|
+
if (config.agentOverrides !== null) {
|
|
86
|
+
const roleOverrides = config.agentOverrides[workflow.name];
|
|
87
|
+
if (roleOverrides !== undefined && roleOverrides[role] !== undefined) {
|
|
88
|
+
alias = roleOverrides[role];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const agentConfig = config.agents[alias];
|
|
92
|
+
if (agentConfig === undefined) {
|
|
93
|
+
fail(`unknown agent alias in config: ${alias}`);
|
|
94
|
+
}
|
|
95
|
+
return { host: agentConfig.host, gateway: agentConfig.gateway, cwd };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Path to the broker session store DB under the storage root. Mirrors the
|
|
99
|
+
* default used by `createSessionStore` but anchored at the user's `UWF_HOME`
|
|
100
|
+
* so multi-process scripts share the same SQLite file.
|
|
101
|
+
*/
|
|
102
|
+
export function brokerSessionStorePath(storageRoot) {
|
|
103
|
+
return join(storageRoot, "broker", "sessions.db");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Open (or create) the broker session store under `<storageRoot>/broker/sessions.db`.
|
|
107
|
+
* The caller is responsible for closing it.
|
|
108
|
+
*/
|
|
109
|
+
export function openBrokerSessionStore(storageRoot) {
|
|
110
|
+
return createSessionStore({ dbPath: brokerSessionStorePath(storageRoot) });
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Look up the role's frontmatter / output schema in CAS so we can drive
|
|
114
|
+
* `tryFrontmatterFastPath`. The workflow payload only carries the schema's
|
|
115
|
+
* CAS hash; the JSON Schema itself lives in CAS via `WorkflowAdd`.
|
|
116
|
+
*/
|
|
117
|
+
function loadRoleSchemaHash(workflow, role) {
|
|
118
|
+
const roleDef = workflow.roles[role];
|
|
119
|
+
if (roleDef === undefined) {
|
|
120
|
+
fail(`unknown role "${role}" in workflow "${workflow.name}"`);
|
|
121
|
+
}
|
|
122
|
+
return roleDef.frontmatter;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Build the output-format instruction for a role from its frontmatter schema in
|
|
126
|
+
* CAS. Returns an empty string when the schema node is missing.
|
|
127
|
+
*/
|
|
128
|
+
function loadOutputFormatInstruction(uwf, schemaHash) {
|
|
129
|
+
const node = uwf.store.cas.get(schemaHash);
|
|
130
|
+
if (node === null) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
return buildOutputFormatInstruction(node.payload);
|
|
134
|
+
}
|
|
135
|
+
/** Extract the last assistant turn's content from a detail node, or null. */
|
|
136
|
+
function extractStepContent(uwf, detailRef) {
|
|
137
|
+
const detailNode = uwf.store.cas.get(detailRef);
|
|
138
|
+
if (detailNode === null) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const detail = detailNode.payload;
|
|
142
|
+
const turns = detail.turns;
|
|
143
|
+
if (!Array.isArray(turns) || turns.length === 0) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
147
|
+
const turnRef = turns[i];
|
|
148
|
+
if (typeof turnRef !== "string") {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const turnNode = uwf.store.cas.get(turnRef);
|
|
152
|
+
if (turnNode === null) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const turn = turnNode.payload;
|
|
156
|
+
if (turn.role === "assistant" &&
|
|
157
|
+
typeof turn.content === "string" &&
|
|
158
|
+
turn.content.trim() !== "") {
|
|
159
|
+
return turn.content;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Walk the CAS step chain from `prevHash` back to the StartNode and return the
|
|
166
|
+
* steps in chronological order (oldest first) as StepContext records. Honors the
|
|
167
|
+
* caller-supplied `prev` pointer so poke replace-semantics (prev = old head's
|
|
168
|
+
* prev) produce the correct history. Mirrors the history assembly in
|
|
169
|
+
* util-agent's `buildContext`, but reuses the store the CLI already opened.
|
|
170
|
+
*/
|
|
171
|
+
function collectStepContexts(uwf, prevHash) {
|
|
172
|
+
const newestFirst = [];
|
|
173
|
+
let hash = prevHash;
|
|
174
|
+
while (hash !== null) {
|
|
175
|
+
const node = uwf.store.cas.get(hash);
|
|
176
|
+
if (node === null || node.type !== uwf.schemas.stepNode) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
const payload = node.payload;
|
|
180
|
+
newestFirst.push(payload);
|
|
181
|
+
hash = payload.prev;
|
|
182
|
+
}
|
|
183
|
+
const chronological = [...newestFirst].reverse();
|
|
184
|
+
return chronological.map((step) => ({
|
|
185
|
+
role: step.role,
|
|
186
|
+
output: expandOutput(uwf, step.output),
|
|
187
|
+
detail: step.detail,
|
|
188
|
+
agent: step.agent,
|
|
189
|
+
edgePrompt: step.edgePrompt ?? "",
|
|
190
|
+
startedAtMs: step.startedAtMs,
|
|
191
|
+
completedAtMs: step.completedAtMs,
|
|
192
|
+
cwd: step.cwd ?? "",
|
|
193
|
+
assembledPrompt: step.assembledPrompt ?? null,
|
|
194
|
+
usage: step.usage ?? null,
|
|
195
|
+
previousAttempts: step.previousAttempts ?? null,
|
|
196
|
+
content: extractStepContent(uwf, step.detail),
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Assemble the full agent prompt for a broker step. Combines the five
|
|
201
|
+
* components the legacy agent-CLI path produced (output-format instruction,
|
|
202
|
+
* thread progress, role prompt, task prompt, and continuation/edge context) so
|
|
203
|
+
* `broker.send()` receives the same context the spawned-agent path did.
|
|
204
|
+
*
|
|
205
|
+
* Mirrors `buildClaudeCodePrompt` from the agent-claude-code adapter.
|
|
206
|
+
*/
|
|
207
|
+
export function assembleBrokerPrompt(args) {
|
|
208
|
+
const roleDef = args.workflow.roles[args.role];
|
|
209
|
+
const rolePrompt = roleDef !== undefined ? buildRolePrompt(roleDef) : "";
|
|
210
|
+
const isFirstVisit = !args.steps.some((s) => s.role === args.role);
|
|
211
|
+
const parts = [];
|
|
212
|
+
if (args.outputFormatInstruction !== "") {
|
|
213
|
+
parts.push(args.outputFormatInstruction, "");
|
|
214
|
+
}
|
|
215
|
+
// Inject thread progress so the agent knows step count and role visit count.
|
|
216
|
+
parts.push(buildThreadProgress(args.steps, args.role, args.threadId), "");
|
|
217
|
+
parts.push(rolePrompt, "", "## Task", args.startPrompt);
|
|
218
|
+
if (!isFirstVisit) {
|
|
219
|
+
// Re-entry (broker resumes the cached session): show only steps since the
|
|
220
|
+
// last visit, meta only.
|
|
221
|
+
parts.push("", buildContinuationPrompt(args.steps, args.role, args.edgePrompt));
|
|
222
|
+
}
|
|
223
|
+
else if (args.steps.length > 0) {
|
|
224
|
+
// First visit with prior history: show steps with content for recent ones.
|
|
225
|
+
parts.push("", buildContinuationPrompt(args.steps, args.role, args.edgePrompt, {
|
|
226
|
+
includeContent: true,
|
|
227
|
+
quota: 32000,
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
parts.push("", "## Current Instruction", "", args.edgePrompt);
|
|
232
|
+
}
|
|
233
|
+
return parts.join("\n");
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Persist the step's detail node. Phase 2 (#419): the detail no longer contains
|
|
237
|
+
* a `turns` array — turns are self-contained via their `prev`+`owner` chain.
|
|
238
|
+
* Only metadata (sessionId, duration, turnCount) is stored.
|
|
239
|
+
*/
|
|
240
|
+
async function storeBrokerDetail(uwf, result, threadId, role, startedAtMs, completedAtMs, turnCount) {
|
|
241
|
+
const detailSchemaHash = await putSchema(uwf.store, DETAIL_SCHEMA);
|
|
242
|
+
// Phase 2 (#419): clear the deprecated role-keyed active var for backward
|
|
243
|
+
// compatibility. The turns are already persisted via the turn chain.
|
|
244
|
+
clearActiveTurns(uwf.store, threadId, role);
|
|
245
|
+
const detail = {
|
|
246
|
+
sessionId: result.sessionId,
|
|
247
|
+
duration: Math.max(0, completedAtMs - startedAtMs),
|
|
248
|
+
turnCount,
|
|
249
|
+
};
|
|
250
|
+
return uwf.store.cas.put(detailSchemaHash, detail);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Build the realtime `onTurn` callback wired into `broker.send` (Phase 2, #419).
|
|
254
|
+
* For each arriving assistant turn it writes a TurnNode with:
|
|
255
|
+
* - `role: "assistant"`
|
|
256
|
+
* - `content: <turn content>`
|
|
257
|
+
* - `prev: <previous turn hash or null>`
|
|
258
|
+
* - `owner: <current step-start hash>`
|
|
259
|
+
* Then updates `@uwf/active-turn-head/<threadId>` to point to the new turn.
|
|
260
|
+
*
|
|
261
|
+
* The turn chain is self-contained — each turn links to its predecessor via
|
|
262
|
+
* `prev` and to its owning step via `owner`. No separate array accumulation
|
|
263
|
+
* is needed.
|
|
264
|
+
*
|
|
265
|
+
* Returns the turn count after the step completes (for detail node).
|
|
266
|
+
*/
|
|
267
|
+
function makeOnTurn(uwf, threadId, stepStartHash) {
|
|
268
|
+
let turnCount = 0;
|
|
269
|
+
// Get the current turn head before this step starts (could be from previous steps)
|
|
270
|
+
let prevTurnHash = getActiveTurnHead(uwf.store, threadId);
|
|
271
|
+
const onTurn = (turn) => {
|
|
272
|
+
// Write turn node with prev+owner chain
|
|
273
|
+
const turnHash = writeTurnNode(uwf, {
|
|
274
|
+
role: "assistant",
|
|
275
|
+
content: turn.content,
|
|
276
|
+
prev: prevTurnHash,
|
|
277
|
+
owner: stepStartHash,
|
|
278
|
+
});
|
|
279
|
+
// Update thread-keyed active turn head
|
|
280
|
+
setActiveTurnHead(uwf.store, threadId, turnHash);
|
|
281
|
+
// Also maintain deprecated role-keyed var for backward compatibility
|
|
282
|
+
// during transition period (can be removed in Phase 3)
|
|
283
|
+
// appendActiveTurn is called but we don't rely on it for turn retrieval
|
|
284
|
+
prevTurnHash = turnHash;
|
|
285
|
+
turnCount++;
|
|
286
|
+
};
|
|
287
|
+
const getTurnCount = () => turnCount;
|
|
288
|
+
return { onTurn, getTurnCount };
|
|
289
|
+
}
|
|
290
|
+
/** Persist a StepNode payload and verify it round-trips through schema validation. */
|
|
291
|
+
async function writeBrokerStepNode(args) {
|
|
292
|
+
const payload = {
|
|
293
|
+
start: args.startHash,
|
|
294
|
+
prev: args.prevHash,
|
|
295
|
+
role: args.role,
|
|
296
|
+
output: args.outputHash,
|
|
297
|
+
detail: args.detailHash,
|
|
298
|
+
agent: args.agentName,
|
|
299
|
+
edgePrompt: args.edgePrompt,
|
|
300
|
+
startedAtMs: args.startedAtMs,
|
|
301
|
+
completedAtMs: args.completedAtMs,
|
|
302
|
+
cwd: args.cwd,
|
|
303
|
+
assembledPrompt: args.assembledPromptHash,
|
|
304
|
+
usage: args.usage,
|
|
305
|
+
previousAttempts: args.previousAttempts,
|
|
306
|
+
};
|
|
307
|
+
const hash = await args.uwf.store.cas.put(args.uwf.schemas.stepNode, payload);
|
|
308
|
+
const node = args.uwf.store.cas.get(hash);
|
|
309
|
+
if (node === null || !validate(args.uwf.store, node)) {
|
|
310
|
+
fail("broker step persisted a StepNode that failed schema validation");
|
|
311
|
+
}
|
|
312
|
+
return hash;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Render the engine-level suspend (coroutine yield) wire format — frontmatter
|
|
316
|
+
* with `$status: "$SUSPEND"` plus a human-readable `reason`. Round-trips through
|
|
317
|
+
* the public {@link trySuspendFastPath}, which stores it against the reserved
|
|
318
|
+
* suspend-output schema.
|
|
319
|
+
*
|
|
320
|
+
* NOTE: this mirrors the adapter-side `buildSuspendOutput` in
|
|
321
|
+
* `@united-workforce/util-agent`, kept private here on purpose — the #381
|
|
322
|
+
* public-API cleanup deliberately keeps that helper OUT of the util-agent
|
|
323
|
+
* barrel, and `broker-step.ts` is engine/CLI code (not an adapter). The string
|
|
324
|
+
* is a one-liner over `SUSPEND_STATUS`, so duplicating it costs nothing and
|
|
325
|
+
* preserves the package boundary (see public-api-no-llm.test.ts).
|
|
326
|
+
*/
|
|
327
|
+
function buildSuspendOutput(reason) {
|
|
328
|
+
return `---\n$status: ${SUSPEND_STATUS}\nreason: ${reason}\n---\n`;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Route a broker `kind:"suspended"` result through the existing engine-level
|
|
332
|
+
* `$SUSPEND` exit (issue #435, Phase 2). A send timeout is NOT an error and NOT
|
|
333
|
+
* a frontmatter failure — it is a human gate. We build a suspend output via the
|
|
334
|
+
* shared {@link buildSuspendOutput} / {@link trySuspendFastPath} helpers (the
|
|
335
|
+
* same wire format any agent that prints `$status: "$SUSPEND"` produces), store
|
|
336
|
+
* it against the reserved `suspendOutput` schema, record `nativeId`/`elapsedMs`
|
|
337
|
+
* on the detail node for diagnostics, and persist a normal StepNode. Downstream
|
|
338
|
+
* thread-status resolution maps the head step's `$status: "$SUSPEND"` output to
|
|
339
|
+
* `status: "suspended"`, and `uwf thread resume` continues from `nativeId`.
|
|
340
|
+
*/
|
|
341
|
+
async function writeSuspendedStep(args) {
|
|
342
|
+
const reason = `sumeru send timed out after ${args.suspend.elapsedMs}ms ` +
|
|
343
|
+
`(nativeId=${args.suspend.nativeId}); resume to continue`;
|
|
344
|
+
const suspendRaw = buildSuspendOutput(reason);
|
|
345
|
+
const extracted = await trySuspendFastPath(suspendRaw, args.uwf.schemas.suspendOutput, args.uwf.store);
|
|
346
|
+
if (extracted === null) {
|
|
347
|
+
fail("broker step failed to build a $SUSPEND output node for a timeout-suspended send");
|
|
348
|
+
}
|
|
349
|
+
const detailSchemaHash = await putSchema(args.uwf.store, DETAIL_SCHEMA);
|
|
350
|
+
// Clear the deprecated role-keyed active var (parity with storeBrokerDetail).
|
|
351
|
+
clearActiveTurns(args.uwf.store, args.threadId, args.role);
|
|
352
|
+
const detail = {
|
|
353
|
+
sessionId: args.sessionId,
|
|
354
|
+
duration: Math.max(0, args.completedAtMs - args.startedAtMs),
|
|
355
|
+
turnCount: args.turnCount,
|
|
356
|
+
nativeId: args.suspend.nativeId,
|
|
357
|
+
elapsedMs: args.suspend.elapsedMs,
|
|
358
|
+
reason: args.suspend.reason,
|
|
359
|
+
};
|
|
360
|
+
const detailHash = await args.uwf.store.cas.put(detailSchemaHash, detail);
|
|
361
|
+
// Clear the active-step var: the step has reached a terminal (suspended) state.
|
|
362
|
+
clearActiveStep(args.uwf.store, args.threadId);
|
|
363
|
+
const stepHash = await writeBrokerStepNode({
|
|
364
|
+
uwf: args.uwf,
|
|
365
|
+
startHash: args.startHash,
|
|
366
|
+
prevHash: args.prevHash,
|
|
367
|
+
role: args.role,
|
|
368
|
+
outputHash: extracted.outputHash,
|
|
369
|
+
detailHash,
|
|
370
|
+
agentName: args.agentName,
|
|
371
|
+
edgePrompt: args.edgePrompt,
|
|
372
|
+
startedAtMs: args.startedAtMs,
|
|
373
|
+
completedAtMs: args.completedAtMs,
|
|
374
|
+
cwd: args.cwd,
|
|
375
|
+
assembledPromptHash: args.assembledPromptHash,
|
|
376
|
+
usage: null,
|
|
377
|
+
previousAttempts: args.previousAttempts,
|
|
378
|
+
});
|
|
379
|
+
return {
|
|
380
|
+
stepHash,
|
|
381
|
+
detailHash,
|
|
382
|
+
role: args.role,
|
|
383
|
+
frontmatter: extracted.frontmatter,
|
|
384
|
+
body: extracted.body,
|
|
385
|
+
startedAtMs: args.startedAtMs,
|
|
386
|
+
completedAtMs: args.completedAtMs,
|
|
387
|
+
usage: null,
|
|
388
|
+
isError: false,
|
|
389
|
+
errorMessage: null,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async function tryExtract(uwf, rawOutput, outputSchema) {
|
|
393
|
+
// `$status: "$SUSPEND"` is a reserved coroutine yield — store it against the
|
|
394
|
+
// suspend schema, bypassing the role's own frontmatter schema.
|
|
395
|
+
const suspend = await trySuspendFastPath(rawOutput, uwf.schemas.suspendOutput, uwf.store);
|
|
396
|
+
if (suspend !== null) {
|
|
397
|
+
return { outputHash: suspend.outputHash, frontmatter: suspend.frontmatter, body: suspend.body };
|
|
398
|
+
}
|
|
399
|
+
const fastPath = await tryFrontmatterFastPath(rawOutput, outputSchema, uwf.store);
|
|
400
|
+
if (fastPath !== null) {
|
|
401
|
+
return {
|
|
402
|
+
outputHash: fastPath.outputHash,
|
|
403
|
+
frontmatter: fastPath.frontmatter,
|
|
404
|
+
body: fastPath.body,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Drive one moderator-resolved role through `broker.send()`, frontmatter
|
|
411
|
+
* extraction (with retries on the same Sumeru session), and StepNode
|
|
412
|
+
* persistence. Returns a `BrokerStepResult` shaped for the existing
|
|
413
|
+
* `executeAndProcessAgentStep` flow.
|
|
414
|
+
*
|
|
415
|
+
* Phase 2 (#419) changes:
|
|
416
|
+
* - Writes step-start node at entry, sets `@uwf/active-step/<threadId>`
|
|
417
|
+
* - Turns are written with `prev`+`owner` chain via `writeTurnNode`
|
|
418
|
+
* - Updates `@uwf/active-turn-head/<threadId>` as turns arrive
|
|
419
|
+
* - Clears `@uwf/active-step/<threadId>` at completion
|
|
420
|
+
* - Detail node no longer contains `turns` array (turns self-contained)
|
|
421
|
+
*
|
|
422
|
+
* Side effects:
|
|
423
|
+
* - inserts a row in the broker session store keyed by (threadId, role)
|
|
424
|
+
* - writes step-start / turns / detail / StepNode to CAS
|
|
425
|
+
* - on extraction failure, persists an error StepNode (isError=true)
|
|
426
|
+
*/
|
|
427
|
+
export async function executeBrokerStep(args) {
|
|
428
|
+
const sessionStore = openBrokerSessionStore(args.storageRoot);
|
|
429
|
+
try {
|
|
430
|
+
const route = resolveAgentRoute(args.config, args.workflow, args.role, args.agentOverride, args.effectiveCwd === "" ? null : args.effectiveCwd);
|
|
431
|
+
const broker = createBroker({
|
|
432
|
+
sessionStore,
|
|
433
|
+
resolveRoute: () => route,
|
|
434
|
+
clientFactory: null,
|
|
435
|
+
});
|
|
436
|
+
args.plog.log(PL_BROKER_SEND, `broker.send role=${args.role} host=${route.host} gateway=${route.gateway}`, null);
|
|
437
|
+
// Assemble the full agent prompt (output-format instruction + thread
|
|
438
|
+
// progress + role prompt + task + continuation/edge context) so the broker
|
|
439
|
+
// path sends the same context the legacy spawned-agent path did, rather than
|
|
440
|
+
// the bare edge prompt.
|
|
441
|
+
const outputSchemaHash = loadRoleSchemaHash(args.workflow, args.role);
|
|
442
|
+
const outputFormatInstruction = loadOutputFormatInstruction(args.uwf, outputSchemaHash);
|
|
443
|
+
const startNode = args.uwf.store.cas.get(args.startHash);
|
|
444
|
+
const startPrompt = startNode !== null ? startNode.payload.prompt : "";
|
|
445
|
+
const steps = collectStepContexts(args.uwf, args.prevHash);
|
|
446
|
+
const assembledPrompt = assembleBrokerPrompt({
|
|
447
|
+
workflow: args.workflow,
|
|
448
|
+
role: args.role,
|
|
449
|
+
threadId: args.threadId,
|
|
450
|
+
startPrompt,
|
|
451
|
+
steps,
|
|
452
|
+
edgePrompt: args.edgePrompt,
|
|
453
|
+
outputFormatInstruction,
|
|
454
|
+
});
|
|
455
|
+
const assembledPromptHash = (await args.uwf.store.cas.put(args.uwf.schemas.text, assembledPrompt));
|
|
456
|
+
const startedAtMs = Date.now();
|
|
457
|
+
// Phase 2 (#419): Write step-start node at entry
|
|
458
|
+
const stepStartHash = writeStepStart(args.uwf, {
|
|
459
|
+
role: args.role,
|
|
460
|
+
edgePrompt: args.edgePrompt,
|
|
461
|
+
stepIndex: steps.length,
|
|
462
|
+
prev: args.prevHash,
|
|
463
|
+
start: args.startHash,
|
|
464
|
+
startedAtMs,
|
|
465
|
+
cwd: args.effectiveCwd,
|
|
466
|
+
});
|
|
467
|
+
// Set the active-step var so other processes can detect in-flight state
|
|
468
|
+
setActiveStep(args.uwf.store, args.threadId, stepStartHash);
|
|
469
|
+
// Start-of-step clear (Phase 2, #398): a crash-rerun is a fresh attempt, so
|
|
470
|
+
// any residual active var from a failed prior attempt is dropped here —
|
|
471
|
+
// before any onTurn can fire — rather than appended onto. The clear is
|
|
472
|
+
// start-of-step only (NOT per-send): frontmatter retries below re-send on
|
|
473
|
+
// the cached session and must keep appending to the same attempt's var.
|
|
474
|
+
clearActiveTurns(args.uwf.store, args.threadId, args.role);
|
|
475
|
+
// Phase 2 (#419): makeOnTurn now writes turns with prev+owner chain
|
|
476
|
+
const { onTurn, getTurnCount } = makeOnTurn(args.uwf, args.threadId, stepStartHash);
|
|
477
|
+
const primary = await broker.send({
|
|
478
|
+
threadId: args.threadId,
|
|
479
|
+
role: args.role,
|
|
480
|
+
prompt: assembledPrompt,
|
|
481
|
+
onTurn,
|
|
482
|
+
});
|
|
483
|
+
// Suspend gate (issue #435, Phase 2): a broker `kind:"suspended"` result
|
|
484
|
+
// means the Sumeru send hit a timeout and emitted RFC #95 `suspend`. Route
|
|
485
|
+
// it through the existing `$SUSPEND` exit BEFORE any frontmatter work —
|
|
486
|
+
// suspend is a human gate, never retried, never an error. TypeScript's
|
|
487
|
+
// discriminated union forces this narrow before any `primary.output` read.
|
|
488
|
+
if (primary.kind === "suspended") {
|
|
489
|
+
return writeSuspendedStep({
|
|
490
|
+
uwf: args.uwf,
|
|
491
|
+
threadId: args.threadId,
|
|
492
|
+
suspend: {
|
|
493
|
+
reason: primary.reason,
|
|
494
|
+
nativeId: primary.nativeId,
|
|
495
|
+
elapsedMs: primary.elapsedMs,
|
|
496
|
+
},
|
|
497
|
+
sessionId: primary.sessionId,
|
|
498
|
+
turnCount: getTurnCount(),
|
|
499
|
+
startHash: args.startHash,
|
|
500
|
+
prevHash: args.prevHash,
|
|
501
|
+
role: args.role,
|
|
502
|
+
agentName: route.gateway,
|
|
503
|
+
edgePrompt: args.edgePrompt,
|
|
504
|
+
startedAtMs,
|
|
505
|
+
completedAtMs: Date.now(),
|
|
506
|
+
cwd: args.effectiveCwd,
|
|
507
|
+
assembledPromptHash,
|
|
508
|
+
previousAttempts: args.previousAttempts,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
let extracted = await tryExtract(args.uwf, primary.output, outputSchemaHash);
|
|
512
|
+
let accumulatedUsage = brokerUsage(primary);
|
|
513
|
+
let lastOutput = primary.output;
|
|
514
|
+
let lastSessionId = primary.sessionId;
|
|
515
|
+
// Retry on the same (threadId, role) — the broker re-uses the cached
|
|
516
|
+
// Sumeru session, so the agent gets to "fix its frontmatter" with full
|
|
517
|
+
// context preserved. Retries carry the same onTurn and keep appending to
|
|
518
|
+
// the same attempt's active var (no clear between retries).
|
|
519
|
+
for (let retry = 0; retry < MAX_FRONTMATTER_RETRIES && extracted === null; retry++) {
|
|
520
|
+
const correctionPrompt = buildFrontmatterRetryPrompt(outputFormatInstruction);
|
|
521
|
+
log(PL_FRONTMATTER_RETRY, `frontmatter retry ${retry + 1}/${MAX_FRONTMATTER_RETRIES} thread=${args.threadId} role=${args.role}`);
|
|
522
|
+
const retryResult = await broker.send({
|
|
523
|
+
threadId: args.threadId,
|
|
524
|
+
role: args.role,
|
|
525
|
+
prompt: correctionPrompt,
|
|
526
|
+
onTurn,
|
|
527
|
+
});
|
|
528
|
+
// A retry can itself time out — honor the same suspend gate rather than
|
|
529
|
+
// dereferencing `retryResult.output` on a suspended result.
|
|
530
|
+
if (retryResult.kind === "suspended") {
|
|
531
|
+
return writeSuspendedStep({
|
|
532
|
+
uwf: args.uwf,
|
|
533
|
+
threadId: args.threadId,
|
|
534
|
+
suspend: {
|
|
535
|
+
reason: retryResult.reason,
|
|
536
|
+
nativeId: retryResult.nativeId,
|
|
537
|
+
elapsedMs: retryResult.elapsedMs,
|
|
538
|
+
},
|
|
539
|
+
sessionId: retryResult.sessionId,
|
|
540
|
+
turnCount: getTurnCount(),
|
|
541
|
+
startHash: args.startHash,
|
|
542
|
+
prevHash: args.prevHash,
|
|
543
|
+
role: args.role,
|
|
544
|
+
agentName: route.gateway,
|
|
545
|
+
edgePrompt: args.edgePrompt,
|
|
546
|
+
startedAtMs,
|
|
547
|
+
completedAtMs: Date.now(),
|
|
548
|
+
cwd: args.effectiveCwd,
|
|
549
|
+
assembledPromptHash,
|
|
550
|
+
previousAttempts: args.previousAttempts,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
lastOutput = retryResult.output;
|
|
554
|
+
lastSessionId = retryResult.sessionId;
|
|
555
|
+
accumulatedUsage = mergeUsage(accumulatedUsage, brokerUsage(retryResult));
|
|
556
|
+
extracted = await tryExtract(args.uwf, lastOutput, outputSchemaHash);
|
|
557
|
+
}
|
|
558
|
+
const completedAtMs = Date.now();
|
|
559
|
+
// Phase 2 (#419): Pass turn count to detail (no longer from active var)
|
|
560
|
+
const detailHash = await storeBrokerDetail(args.uwf, { ...primary, output: lastOutput, sessionId: lastSessionId }, args.threadId, args.role, startedAtMs, completedAtMs, getTurnCount());
|
|
561
|
+
// Phase 2 (#419): Clear active-step var on completion
|
|
562
|
+
clearActiveStep(args.uwf.store, args.threadId);
|
|
563
|
+
if (extracted === null) {
|
|
564
|
+
log(PL_FRONTMATTER_FAIL, `frontmatter extraction failed after ${MAX_FRONTMATTER_RETRIES} retries thread=${args.threadId} role=${args.role}`);
|
|
565
|
+
const errorMessage = "Agent output does not contain valid YAML frontmatter matching the role schema " +
|
|
566
|
+
`after ${MAX_FRONTMATTER_RETRIES} retries.\n` +
|
|
567
|
+
`Raw output (first 500 chars): ${lastOutput.slice(0, 500)}`;
|
|
568
|
+
const errorPayload = {
|
|
569
|
+
$status: "error",
|
|
570
|
+
error: errorMessage,
|
|
571
|
+
phase: "frontmatter_extraction",
|
|
572
|
+
};
|
|
573
|
+
const errorOutputHash = await args.uwf.store.cas.put(args.uwf.schemas.errorOutput, errorPayload);
|
|
574
|
+
const failedStepHash = await writeBrokerStepNode({
|
|
575
|
+
uwf: args.uwf,
|
|
576
|
+
startHash: args.startHash,
|
|
577
|
+
prevHash: args.prevHash,
|
|
578
|
+
role: args.role,
|
|
579
|
+
outputHash: errorOutputHash,
|
|
580
|
+
detailHash,
|
|
581
|
+
agentName: route.gateway,
|
|
582
|
+
edgePrompt: args.edgePrompt,
|
|
583
|
+
startedAtMs,
|
|
584
|
+
completedAtMs,
|
|
585
|
+
cwd: args.effectiveCwd,
|
|
586
|
+
assembledPromptHash,
|
|
587
|
+
usage: accumulatedUsage,
|
|
588
|
+
previousAttempts: null,
|
|
589
|
+
});
|
|
590
|
+
return {
|
|
591
|
+
stepHash: failedStepHash,
|
|
592
|
+
detailHash,
|
|
593
|
+
role: args.role,
|
|
594
|
+
frontmatter: { $status: "error" },
|
|
595
|
+
body: "",
|
|
596
|
+
startedAtMs,
|
|
597
|
+
completedAtMs,
|
|
598
|
+
usage: accumulatedUsage,
|
|
599
|
+
isError: true,
|
|
600
|
+
errorMessage,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const stepHash = await writeBrokerStepNode({
|
|
604
|
+
uwf: args.uwf,
|
|
605
|
+
startHash: args.startHash,
|
|
606
|
+
prevHash: args.prevHash,
|
|
607
|
+
role: args.role,
|
|
608
|
+
outputHash: extracted.outputHash,
|
|
609
|
+
detailHash,
|
|
610
|
+
agentName: route.gateway,
|
|
611
|
+
edgePrompt: args.edgePrompt,
|
|
612
|
+
startedAtMs,
|
|
613
|
+
completedAtMs,
|
|
614
|
+
cwd: args.effectiveCwd,
|
|
615
|
+
assembledPromptHash,
|
|
616
|
+
usage: accumulatedUsage,
|
|
617
|
+
previousAttempts: args.previousAttempts,
|
|
618
|
+
});
|
|
619
|
+
return {
|
|
620
|
+
stepHash,
|
|
621
|
+
detailHash,
|
|
622
|
+
role: args.role,
|
|
623
|
+
frontmatter: extracted.frontmatter,
|
|
624
|
+
body: extracted.body,
|
|
625
|
+
startedAtMs,
|
|
626
|
+
completedAtMs,
|
|
627
|
+
usage: accumulatedUsage,
|
|
628
|
+
isError: false,
|
|
629
|
+
errorMessage: null,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
finally {
|
|
633
|
+
sessionStore.close();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function brokerUsage(result) {
|
|
637
|
+
// Sumeru's `done` event reports per-exchange usage. Normalize into the
|
|
638
|
+
// engine's Usage shape so `mergeUsage` can sum across retries. A suspended
|
|
639
|
+
// result has no `done` (the discriminated union enforces this narrow) — a
|
|
640
|
+
// timeout carries no usage summary.
|
|
641
|
+
if (result.kind !== "completed") {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
const done = result.done;
|
|
645
|
+
if (done === null || typeof done !== "object") {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
const turns = done.turnCount;
|
|
649
|
+
const inputTokens = done.tokens !== null ? done.tokens.in : 0;
|
|
650
|
+
const outputTokens = done.tokens !== null ? done.tokens.out : 0;
|
|
651
|
+
const duration = done.durationMs;
|
|
652
|
+
return { turns, inputTokens, outputTokens, duration };
|
|
653
|
+
}
|
|
654
|
+
//# sourceMappingURL=broker-step.js.map
|