@united-workforce/cli 0.6.1 → 0.7.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 +89 -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__/config.test.js +33 -37
- package/dist/__tests__/config.test.js.map +1 -1
- 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__/setup-agent-discovery.test.js +17 -5
- 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__/thread-agent-failure-suspended.test.js +3 -3
- package/dist/__tests__/thread-agent-failure-suspended.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/commands/broker-step.d.ts +110 -0
- package/dist/commands/broker-step.d.ts.map +1 -0
- package/dist/commands/broker-step.js +450 -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.js +3 -3
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +8 -1
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +6 -5
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +11 -154
- 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/package.json +5 -4
- package/src/__tests__/agent-resolution-llm-free.test.ts +14 -2
- package/src/__tests__/broker-prompt.test.ts +142 -0
- package/src/__tests__/config.test.ts +35 -39
- package/src/__tests__/e2e-broker-step.test.ts +320 -0
- package/src/__tests__/e2e-mock-agent.test.ts +1 -1
- package/src/__tests__/setup-agent-discovery.test.ts +17 -5
- package/src/__tests__/setup-no-llm.test.ts +5 -2
- package/src/__tests__/step-ask.test.ts +9 -6
- package/src/__tests__/thread-agent-failure-suspended.test.ts +3 -3
- 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/commands/broker-step.ts +636 -0
- package/src/commands/config.ts +2 -24
- package/src/commands/prompt.ts +3 -3
- package/src/commands/setup.ts +9 -1
- package/src/commands/step.ts +21 -204
- package/src/commands/thread.ts +87 -192
- package/dist/.build-fingerprint +0 -1
- 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
package/src/commands/setup.ts
CHANGED
|
@@ -205,6 +205,14 @@ export async function _promptAgentSelection(
|
|
|
205
205
|
|
|
206
206
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Default Sumeru host used when seeding a fresh agents.<alias> entry.
|
|
210
|
+
* Phase 3 (#380) breaking change — agents are routed through the broker via
|
|
211
|
+
* `host` + `gateway`, replacing the legacy `command` + `args` CLI binary
|
|
212
|
+
* path.
|
|
213
|
+
*/
|
|
214
|
+
const DEFAULT_SUMERU_HOST = "http://127.0.0.1:7900";
|
|
215
|
+
|
|
208
216
|
/**
|
|
209
217
|
* Merge setup args into config.yaml structure. Non-destructive — preserves
|
|
210
218
|
* existing entries (including agentOverrides). Engine config is LLM-free, so
|
|
@@ -219,7 +227,7 @@ function mergeConfig(existing: Record<string, unknown>, args: SetupArgs): Record
|
|
|
219
227
|
|
|
220
228
|
const agentName = _agentNameFromBinary(args.agent);
|
|
221
229
|
if (!agents[agentName]) {
|
|
222
|
-
agents[agentName] = {
|
|
230
|
+
agents[agentName] = { host: DEFAULT_SUMERU_HOST, gateway: agentName };
|
|
223
231
|
}
|
|
224
232
|
|
|
225
233
|
const merged: Record<string, unknown> = {
|
package/src/commands/step.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import type { CasStore } from "@ocas/core";
|
|
3
2
|
import type {
|
|
4
|
-
AgentAlias,
|
|
5
|
-
AgentConfig,
|
|
6
3
|
CasRef,
|
|
7
4
|
StartEntry,
|
|
8
5
|
StepEntry,
|
|
@@ -10,11 +7,8 @@ import type {
|
|
|
10
7
|
ThreadForkOutput,
|
|
11
8
|
ThreadId,
|
|
12
9
|
ThreadStepsOutput,
|
|
13
|
-
WorkflowConfig,
|
|
14
|
-
WorkflowPayload,
|
|
15
10
|
} from "@united-workforce/protocol";
|
|
16
11
|
import { createLogger, generateUlid } from "@united-workforce/util";
|
|
17
|
-
import { getAskSessionId, loadWorkflowConfig, setAskSessionId } from "@united-workforce/util-agent";
|
|
18
12
|
import { createUwfStore, setThread, type UwfStore } from "../store.js";
|
|
19
13
|
import {
|
|
20
14
|
collectOrderedSteps,
|
|
@@ -456,148 +450,13 @@ export async function cmdStepRead(
|
|
|
456
450
|
}
|
|
457
451
|
|
|
458
452
|
// ── step ask ────────────────────────────────────────────────────────────────
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (command === undefined) {
|
|
467
|
-
fail("agent override must not be empty");
|
|
468
|
-
}
|
|
469
|
-
return { command, args: parts.slice(1) };
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function resolveAskAgentConfig(
|
|
473
|
-
config: WorkflowConfig,
|
|
474
|
-
workflow: WorkflowPayload | null,
|
|
475
|
-
role: string,
|
|
476
|
-
agentOverride: string | null,
|
|
477
|
-
recordedAgent: string,
|
|
478
|
-
): AgentConfig {
|
|
479
|
-
if (agentOverride !== null) {
|
|
480
|
-
const fromAlias = config.agents[agentOverride as AgentAlias];
|
|
481
|
-
if (fromAlias !== undefined) {
|
|
482
|
-
return fromAlias;
|
|
483
|
-
}
|
|
484
|
-
return parseAgentOverride(agentOverride);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Try to resolve via the recorded agent name as a config alias.
|
|
488
|
-
const fromRecorded = config.agents[recordedAgent as AgentAlias];
|
|
489
|
-
if (fromRecorded !== undefined) {
|
|
490
|
-
return fromRecorded;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Fall back to default agent for the workflow / role.
|
|
494
|
-
if (workflow !== null && config.agentOverrides !== null) {
|
|
495
|
-
const roleOverrides = config.agentOverrides[workflow.name];
|
|
496
|
-
if (roleOverrides !== undefined && roleOverrides[role] !== undefined) {
|
|
497
|
-
const alias = roleOverrides[role];
|
|
498
|
-
const agentConfig = config.agents[alias];
|
|
499
|
-
if (agentConfig !== undefined) {
|
|
500
|
-
return agentConfig;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Treat the recorded value as a raw command path.
|
|
506
|
-
return parseAgentOverride(recordedAgent);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Derive the agent name used for cache file partitioning from an executable
|
|
511
|
-
* path or alias. Examples:
|
|
512
|
-
* uwf-hermes → hermes
|
|
513
|
-
* uwf-claude-code → claude-code
|
|
514
|
-
* /tmp/mock-agent.sh → mock
|
|
515
|
-
* /usr/bin/agent → agent
|
|
516
|
-
*/
|
|
517
|
-
function deriveAgentName(commandPath: string): string {
|
|
518
|
-
const basename = commandPath.split(/[/\\]/).pop() ?? commandPath;
|
|
519
|
-
// Strip a trailing extension (.sh, .js, .mjs, .cjs)
|
|
520
|
-
const noExt = basename.replace(/\.(sh|js|mjs|cjs|ts)$/i, "");
|
|
521
|
-
// Strip the `uwf-` prefix introduced by agentLabel().
|
|
522
|
-
const noPrefix = noExt.startsWith("uwf-") ? noExt.slice(4) : noExt;
|
|
523
|
-
// Strip the trailing `-agent` suffix used by tests / generic agent shells.
|
|
524
|
-
const noSuffix = noPrefix.endsWith("-agent") ? noPrefix.slice(0, -"-agent".length) : noPrefix;
|
|
525
|
-
return noSuffix === "" ? noExt : noSuffix;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function loadDetailNode(
|
|
529
|
-
store: CasStore,
|
|
530
|
-
detailRef: CasRef,
|
|
531
|
-
): { sessionId: string | null; payload: Record<string, unknown> } {
|
|
532
|
-
const detailNode = store.get(detailRef);
|
|
533
|
-
if (detailNode === null) {
|
|
534
|
-
fail(`detail node not found: ${detailRef}`);
|
|
535
|
-
}
|
|
536
|
-
const payload = detailNode.payload as Record<string, unknown>;
|
|
537
|
-
const sessionId = typeof payload.sessionId === "string" ? payload.sessionId : null;
|
|
538
|
-
return { sessionId, payload };
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function spawnAskAgent(agent: AgentConfig, argv: string[], cwd: string): { stdout: string } {
|
|
542
|
-
try {
|
|
543
|
-
const stdout = execFileSync(agent.command, [...agent.args, ...argv], {
|
|
544
|
-
encoding: "utf8",
|
|
545
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
546
|
-
maxBuffer: 50 * 1024 * 1024,
|
|
547
|
-
cwd,
|
|
548
|
-
});
|
|
549
|
-
return { stdout };
|
|
550
|
-
} catch (e) {
|
|
551
|
-
const err = e as NodeJS.ErrnoException & { stderr: Buffer | string | null };
|
|
552
|
-
if (err.code === "ENOENT") {
|
|
553
|
-
fail(
|
|
554
|
-
`"${agent.command}" not found in PATH. Install it or check your PATH config. Run: which ${agent.command}`,
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
const stderr =
|
|
558
|
-
err.stderr == null
|
|
559
|
-
? ""
|
|
560
|
-
: typeof err.stderr === "string"
|
|
561
|
-
? err.stderr
|
|
562
|
-
: err.stderr.toString("utf8");
|
|
563
|
-
const detail = stderr.trim() !== "" ? `: ${stderr.trim()}` : "";
|
|
564
|
-
fail(`agent command failed (${agent.command})${detail}`);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function resolveAskWorkflow(uwf: UwfStore, payload: StepNodePayload): WorkflowPayload | null {
|
|
569
|
-
const startNode = uwf.store.cas.get(payload.start);
|
|
570
|
-
if (startNode === null) {
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
const start = startNode.payload as { workflow: CasRef };
|
|
574
|
-
const workflowNode = uwf.store.cas.get(start.workflow);
|
|
575
|
-
if (workflowNode === null) {
|
|
576
|
-
return null;
|
|
577
|
-
}
|
|
578
|
-
return workflowNode.payload as WorkflowPayload;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
async function performFork(
|
|
582
|
-
agent: AgentConfig,
|
|
583
|
-
agentName: string,
|
|
584
|
-
stepHash: CasRef,
|
|
585
|
-
sourceSessionId: string,
|
|
586
|
-
storageRoot: string,
|
|
587
|
-
cwd: string,
|
|
588
|
-
): Promise<string> {
|
|
589
|
-
const cached = await getAskSessionId(agentName, stepHash, storageRoot);
|
|
590
|
-
if (cached !== null) {
|
|
591
|
-
return cached;
|
|
592
|
-
}
|
|
593
|
-
const { stdout } = spawnAskAgent(agent, ["--mode", "fork", "--session", sourceSessionId], cwd);
|
|
594
|
-
const newSessionId = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
595
|
-
if (newSessionId === "") {
|
|
596
|
-
fail(`agent fork did not return a session id (${agent.command})`);
|
|
597
|
-
}
|
|
598
|
-
await setAskSessionId(agentName, stepHash, newSessionId, storageRoot);
|
|
599
|
-
return newSessionId;
|
|
600
|
-
}
|
|
453
|
+
//
|
|
454
|
+
// Phase 3 (#380) — Option B: `step ask` is disabled while broker integration
|
|
455
|
+
// lands. The pre-broker spawn-agent path depended on the legacy
|
|
456
|
+
// `agents.<alias>: {command, args}` config shape; that shape was replaced by
|
|
457
|
+
// `{host, gateway}` and the equivalent broker `ask`/`fork` primitives are
|
|
458
|
+
// scheduled for Phase 4 (#381). The command exits non-zero with a clear
|
|
459
|
+
// migration pointer so existing scripts fail fast rather than silently.
|
|
601
460
|
|
|
602
461
|
export type CmdStepAskOptions = {
|
|
603
462
|
prompt: string;
|
|
@@ -607,64 +466,22 @@ export type CmdStepAskOptions = {
|
|
|
607
466
|
};
|
|
608
467
|
|
|
609
468
|
/**
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
469
|
+
* `uwf step ask` is unavailable in 0.x while broker integration (#381) is in
|
|
470
|
+
* progress. The legacy spawn-agent code path was removed alongside the
|
|
471
|
+
* `agents.<alias>: {command, args}` config shape. Use `uwf thread exec` /
|
|
472
|
+
* `uwf thread resume` instead — those routes go through `broker.send()` and
|
|
473
|
+
* preserve the Sumeru session.
|
|
614
474
|
*/
|
|
615
475
|
export async function cmdStepAsk(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
476
|
+
_storageRoot: string,
|
|
477
|
+
_stepHash: CasRef,
|
|
478
|
+
_options: CmdStepAskOptions,
|
|
619
479
|
): Promise<string> {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
fail(`node ${stepHash} is not a StepNode`);
|
|
627
|
-
}
|
|
628
|
-
const payload = node.payload as StepNodePayload;
|
|
629
|
-
if (payload.detail === null) {
|
|
630
|
-
fail(`step ${stepHash} has no detail; cannot ask`);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const detailRef = payload.detail;
|
|
634
|
-
const { sessionId: sourceSessionId } = loadDetailNode(uwf.store.cas, detailRef);
|
|
635
|
-
|
|
636
|
-
const workflow = resolveAskWorkflow(uwf, payload);
|
|
637
|
-
const config = await loadWorkflowConfig(storageRoot);
|
|
638
|
-
const agent = resolveAskAgentConfig(
|
|
639
|
-
config,
|
|
640
|
-
workflow,
|
|
641
|
-
payload.role,
|
|
642
|
-
options.agentOverride,
|
|
643
|
-
payload.agent,
|
|
480
|
+
fail(
|
|
481
|
+
"step ask is unavailable in 0.x while broker integration (#381) is in progress. " +
|
|
482
|
+
"The pre-broker spawn-agent path was removed in #380; equivalent ask/fork primitives " +
|
|
483
|
+
"will return in Phase 4 once the Sumeru broker exposes session-fork APIs. " +
|
|
484
|
+
"Use `uwf thread resume <id> -p '...'` to continue a suspended thread, or " +
|
|
485
|
+
"`uwf thread exec <id>` to advance an idle thread.",
|
|
644
486
|
);
|
|
645
|
-
const agentName = deriveAgentName(agent.command);
|
|
646
|
-
|
|
647
|
-
const cwd = payload.cwd !== "" ? payload.cwd : process.cwd();
|
|
648
|
-
|
|
649
|
-
// Fork path: fork (or reuse cached fork) → ask with that session.
|
|
650
|
-
if (options.fork && sourceSessionId !== null) {
|
|
651
|
-
const askSessionId = await performFork(
|
|
652
|
-
agent,
|
|
653
|
-
agentName,
|
|
654
|
-
stepHash,
|
|
655
|
-
sourceSessionId,
|
|
656
|
-
storageRoot,
|
|
657
|
-
cwd,
|
|
658
|
-
);
|
|
659
|
-
const argv = ["--mode", "ask", "--session", askSessionId, "--prompt", options.prompt];
|
|
660
|
-
argv.push("--detail", detailRef);
|
|
661
|
-
const { stdout } = spawnAskAgent(agent, argv, cwd);
|
|
662
|
-
return stdout;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Fallback path: ask without forking; inject detail ref for context.
|
|
666
|
-
const argv = ["--mode", "ask", "--prompt", options.prompt];
|
|
667
|
-
argv.push("--detail", detailRef);
|
|
668
|
-
const { stdout } = spawnAskAgent(agent, argv, cwd);
|
|
669
|
-
return stdout;
|
|
670
487
|
}
|
package/src/commands/thread.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import { access, readFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
4
4
|
import type { VarStore } from "@ocas/core";
|
|
5
5
|
import { validate } from "@ocas/core";
|
|
6
6
|
import type {
|
|
7
|
-
AgentAlias,
|
|
8
|
-
AgentConfig,
|
|
9
7
|
CasRef,
|
|
10
8
|
StartNodePayload,
|
|
11
9
|
StartOutput,
|
|
@@ -31,7 +29,6 @@ import {
|
|
|
31
29
|
generateUlid,
|
|
32
30
|
type ProcessLogger,
|
|
33
31
|
} from "@united-workforce/util";
|
|
34
|
-
import type { AdapterOutput } from "@united-workforce/util-agent";
|
|
35
32
|
import { getEnvPath, loadWorkflowConfig } from "@united-workforce/util-agent";
|
|
36
33
|
import { config as loadDotenv } from "dotenv";
|
|
37
34
|
import { parse } from "yaml";
|
|
@@ -61,6 +58,7 @@ import {
|
|
|
61
58
|
} from "../store.js";
|
|
62
59
|
import { checkWorkflowFilenameConsistency, isCasRef, parseWorkflowPayload } from "../validate.js";
|
|
63
60
|
import { validateWorkflow } from "../validate-semantic.js";
|
|
61
|
+
import { type BrokerStepResult, executeBrokerStep } from "./broker-step.js";
|
|
64
62
|
import {
|
|
65
63
|
getConfigPath,
|
|
66
64
|
getNestedValue,
|
|
@@ -213,7 +211,6 @@ function resolveCurrentRole(uwf: UwfStore, head: CasRef, workflowRef: CasRef): s
|
|
|
213
211
|
|
|
214
212
|
const PL_THREAD_START = "7HNQ4B2X";
|
|
215
213
|
const PL_MODERATOR = "M3K8V9T1";
|
|
216
|
-
const PL_AGENT_SPAWN = "R5J2W8N4";
|
|
217
214
|
const PL_AGENT_DONE = "C6P9E3H7";
|
|
218
215
|
const PL_AGENT_ERROR = "Z3F7K8M2";
|
|
219
216
|
const PL_THREAD_ARCHIVED = "F4D8Q2K5";
|
|
@@ -1120,140 +1117,6 @@ function loadWorkflowPayload(uwf: UwfStore, workflowRef: CasRef): WorkflowPayloa
|
|
|
1120
1117
|
return node.payload as WorkflowPayload;
|
|
1121
1118
|
}
|
|
1122
1119
|
|
|
1123
|
-
function parseAgentOverride(override: string): AgentConfig {
|
|
1124
|
-
const parts = override
|
|
1125
|
-
.trim()
|
|
1126
|
-
.split(/\s+/)
|
|
1127
|
-
.filter((p) => p.length > 0);
|
|
1128
|
-
const command = parts[0];
|
|
1129
|
-
if (command === undefined) {
|
|
1130
|
-
fail("agent override must not be empty");
|
|
1131
|
-
}
|
|
1132
|
-
return { command, args: parts.slice(1) };
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
function resolveAgentConfig(
|
|
1136
|
-
config: WorkflowConfig,
|
|
1137
|
-
workflow: WorkflowPayload,
|
|
1138
|
-
role: string,
|
|
1139
|
-
agentOverride: string | null,
|
|
1140
|
-
): AgentConfig {
|
|
1141
|
-
if (agentOverride !== null) {
|
|
1142
|
-
// Try config alias first (e.g. "hermes" → config.agents.hermes),
|
|
1143
|
-
// then fall back to raw command name (e.g. "uwf-hermes" or "/usr/bin/agent").
|
|
1144
|
-
const fromAlias = config.agents[agentOverride as AgentAlias];
|
|
1145
|
-
if (fromAlias !== undefined) {
|
|
1146
|
-
return fromAlias;
|
|
1147
|
-
}
|
|
1148
|
-
return parseAgentOverride(agentOverride);
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
let alias: AgentAlias = config.defaultAgent;
|
|
1152
|
-
if (config.agentOverrides !== null) {
|
|
1153
|
-
const roleOverrides = config.agentOverrides[workflow.name];
|
|
1154
|
-
if (roleOverrides !== undefined && roleOverrides[role] !== undefined) {
|
|
1155
|
-
alias = roleOverrides[role];
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
const agentConfig = config.agents[alias];
|
|
1160
|
-
if (agentConfig === undefined) {
|
|
1161
|
-
fail(`unknown agent alias in config: ${alias}`);
|
|
1162
|
-
}
|
|
1163
|
-
return agentConfig;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function executeAgentCommand(
|
|
1167
|
-
agent: AgentConfig,
|
|
1168
|
-
argv: readonly string[],
|
|
1169
|
-
cwd: string,
|
|
1170
|
-
plog: ProcessLogger,
|
|
1171
|
-
): string {
|
|
1172
|
-
try {
|
|
1173
|
-
return execFileSync(agent.command, argv, {
|
|
1174
|
-
encoding: "utf8",
|
|
1175
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1176
|
-
maxBuffer: 50 * 1024 * 1024, // 50 MB — stream-json output can be large
|
|
1177
|
-
cwd,
|
|
1178
|
-
});
|
|
1179
|
-
} catch (e) {
|
|
1180
|
-
const err = e as NodeJS.ErrnoException & { stderr?: Buffer | string | null };
|
|
1181
|
-
if (err.code === "ENOENT") {
|
|
1182
|
-
failStep(
|
|
1183
|
-
plog,
|
|
1184
|
-
`"${agent.command}" not found in PATH. Install it or check your PATH config. Run: which ${agent.command}`,
|
|
1185
|
-
);
|
|
1186
|
-
}
|
|
1187
|
-
const stderr =
|
|
1188
|
-
err.stderr == null
|
|
1189
|
-
? ""
|
|
1190
|
-
: typeof err.stderr === "string"
|
|
1191
|
-
? err.stderr
|
|
1192
|
-
: err.stderr.toString("utf8");
|
|
1193
|
-
const detail = stderr.trim() !== "" ? `: ${stderr.trim()}` : "";
|
|
1194
|
-
failStep(plog, `agent command failed (${agent.command})${detail}`);
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
function parseAgentOutput(stdout: string, plog: ProcessLogger): unknown {
|
|
1199
|
-
const line = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
1200
|
-
try {
|
|
1201
|
-
return JSON.parse(line);
|
|
1202
|
-
} catch {
|
|
1203
|
-
failStep(plog, `agent stdout last line is not valid JSON: ${line || "(empty)"}`);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function validateAndNormalizeOutput(
|
|
1208
|
-
parsed: unknown,
|
|
1209
|
-
line: string,
|
|
1210
|
-
plog: ProcessLogger,
|
|
1211
|
-
): AdapterOutput {
|
|
1212
|
-
const obj = parsed as Record<string, unknown>;
|
|
1213
|
-
if (
|
|
1214
|
-
typeof obj !== "object" ||
|
|
1215
|
-
obj === null ||
|
|
1216
|
-
typeof obj.stepHash !== "string" ||
|
|
1217
|
-
!isCasRef(obj.stepHash as string)
|
|
1218
|
-
) {
|
|
1219
|
-
failStep(plog, `agent stdout JSON missing valid stepHash: ${line}`);
|
|
1220
|
-
}
|
|
1221
|
-
// Normalize isError / errorMessage so downstream code can rely on them.
|
|
1222
|
-
// Legacy adapters that don't emit these fields default to isError=false.
|
|
1223
|
-
if (obj.isError !== undefined && typeof obj.isError !== "boolean") {
|
|
1224
|
-
failStep(plog, `agent stdout JSON has non-boolean isError: ${line}`);
|
|
1225
|
-
}
|
|
1226
|
-
if (obj.isError === undefined) {
|
|
1227
|
-
obj.isError = false;
|
|
1228
|
-
}
|
|
1229
|
-
if (
|
|
1230
|
-
obj.errorMessage !== undefined &&
|
|
1231
|
-
obj.errorMessage !== null &&
|
|
1232
|
-
typeof obj.errorMessage !== "string"
|
|
1233
|
-
) {
|
|
1234
|
-
failStep(plog, `agent stdout JSON has non-string errorMessage: ${line}`);
|
|
1235
|
-
}
|
|
1236
|
-
if (obj.errorMessage === undefined) {
|
|
1237
|
-
obj.errorMessage = null;
|
|
1238
|
-
}
|
|
1239
|
-
return obj as unknown as AdapterOutput;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
function spawnAgent(
|
|
1243
|
-
plog: ProcessLogger,
|
|
1244
|
-
agent: AgentConfig,
|
|
1245
|
-
threadId: ThreadId,
|
|
1246
|
-
role: string,
|
|
1247
|
-
edgePrompt: string,
|
|
1248
|
-
cwd: string,
|
|
1249
|
-
): AdapterOutput {
|
|
1250
|
-
const argv = [...agent.args, "--thread", threadId, "--role", role, "--prompt", edgePrompt];
|
|
1251
|
-
const stdout = executeAgentCommand(agent, argv, cwd, plog);
|
|
1252
|
-
const line = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
1253
|
-
const parsed = parseAgentOutput(stdout, plog);
|
|
1254
|
-
return validateAndNormalizeOutput(parsed, line, plog);
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
1120
|
function archiveThread(uwf: UwfStore, threadId: ThreadId, _workflow: CasRef, _head: CasRef): void {
|
|
1258
1121
|
completeThread(uwf.varStore, threadId, "end");
|
|
1259
1122
|
}
|
|
@@ -1412,6 +1275,10 @@ function resolveCurrentRoleFromChain(
|
|
|
1412
1275
|
* replacing the head step's output. The new step's `prev` points to the OLD head's
|
|
1413
1276
|
* `prev` — semantically replacing (not appending to) the head. The moderator is NOT
|
|
1414
1277
|
* re-evaluated for routing; the role of the head step is re-used.
|
|
1278
|
+
*
|
|
1279
|
+
* Phase 3 (#380) — drives the broker via `executeBrokerStep` rather than the
|
|
1280
|
+
* legacy `spawnAgent` path. The replace-semantics StepNode is built directly by
|
|
1281
|
+
* passing `prevHash = oldHeadPayload.prev`, so no post-hoc rewrite is needed.
|
|
1415
1282
|
*/
|
|
1416
1283
|
export async function cmdThreadPoke(
|
|
1417
1284
|
storageRoot: string,
|
|
@@ -1431,59 +1298,71 @@ export async function cmdThreadPoke(
|
|
|
1431
1298
|
context: { thread: threadId, workflow: workflowHash },
|
|
1432
1299
|
});
|
|
1433
1300
|
|
|
1434
|
-
// Resolve the agent: --agent override wins; otherwise read from old head step's `agent` field.
|
|
1435
1301
|
const config = await loadWorkflowConfig(storageRoot);
|
|
1436
1302
|
const workflow = loadWorkflowPayload(uwf, workflowHash);
|
|
1437
1303
|
const role = oldHeadPayload.role;
|
|
1438
|
-
const agent =
|
|
1439
|
-
agentOverride !== null
|
|
1440
|
-
? resolveAgentConfig(config, workflow, role, agentOverride)
|
|
1441
|
-
: parseAgentOverride(oldHeadPayload.agent);
|
|
1442
|
-
|
|
1443
1304
|
const effectiveCwd = oldHeadPayload.cwd !== "" ? oldHeadPayload.cwd : threadCwd;
|
|
1444
1305
|
|
|
1445
|
-
plog.log(PL_THREAD_POKE, `poke role=${role}
|
|
1446
|
-
plog.log(PL_AGENT_SPAWN, `spawning agent command=${agent.command}`, {
|
|
1447
|
-
args: [...agent.args, threadId, role].join(" "),
|
|
1448
|
-
});
|
|
1306
|
+
plog.log(PL_THREAD_POKE, `poke role=${role}`, null);
|
|
1449
1307
|
|
|
1450
1308
|
loadDotenv({ path: getEnvPath(storageRoot) });
|
|
1451
1309
|
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1455
|
-
let
|
|
1310
|
+
// Replace semantics: the new step's `prev` is the OLD head's prev, not the
|
|
1311
|
+
// OLD head itself. `executeBrokerStep` writes the StepNode with this prev,
|
|
1312
|
+
// so no post-hoc rewrite is needed.
|
|
1313
|
+
let result: BrokerStepResult;
|
|
1456
1314
|
try {
|
|
1457
|
-
|
|
1315
|
+
result = await executeBrokerStep({
|
|
1316
|
+
storageRoot,
|
|
1317
|
+
uwf,
|
|
1318
|
+
config,
|
|
1319
|
+
workflow,
|
|
1320
|
+
threadId,
|
|
1321
|
+
role,
|
|
1322
|
+
edgePrompt: prompt,
|
|
1323
|
+
effectiveCwd,
|
|
1324
|
+
startHash: chain.startHash,
|
|
1325
|
+
prevHash: oldHeadPayload.prev,
|
|
1326
|
+
agentOverride,
|
|
1327
|
+
previousAttempts: null,
|
|
1328
|
+
plog,
|
|
1329
|
+
});
|
|
1458
1330
|
} catch (e) {
|
|
1459
1331
|
if (e instanceof StepFailureError) {
|
|
1460
|
-
// Fatal
|
|
1332
|
+
// Fatal broker failure in poke — persist suspended state before propagating
|
|
1461
1333
|
const uwfErr = await createUwfStore(storageRoot);
|
|
1462
1334
|
const errEntry = getThread(uwfErr.varStore, threadId) ?? entry;
|
|
1463
1335
|
setThread(uwfErr.varStore, threadId, markThreadSuspended(errEntry, role, e.message));
|
|
1464
1336
|
}
|
|
1465
1337
|
throw e;
|
|
1466
1338
|
}
|
|
1467
|
-
const agentStepHash = agentResult.stepHash as CasRef;
|
|
1468
1339
|
|
|
1469
|
-
|
|
1340
|
+
const replacedHash = result.stepHash;
|
|
1341
|
+
plog.log(PL_AGENT_DONE, `broker returned head=${replacedHash}`, null);
|
|
1470
1342
|
|
|
1471
1343
|
const uwfAfter = await createUwfStore(storageRoot);
|
|
1472
|
-
const agentNode = uwfAfter.store.cas.get(agentStepHash);
|
|
1473
|
-
if (agentNode === null || agentNode.type !== uwfAfter.schemas.stepNode) {
|
|
1474
|
-
failStep(plog, `agent returned hash that is not a StepNode: ${agentStepHash}`);
|
|
1475
|
-
}
|
|
1476
|
-
const agentPayload = agentNode.payload as StepNodePayload;
|
|
1477
1344
|
|
|
1478
|
-
//
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1345
|
+
// Recoverable broker error: do NOT advance head, persist suspended state.
|
|
1346
|
+
if (result.isError) {
|
|
1347
|
+
const errorMsg = result.errorMessage ?? "broker reported error";
|
|
1348
|
+
plog.log(
|
|
1349
|
+
PL_AGENT_ERROR,
|
|
1350
|
+
`poke recoverable failure stepHash=${replacedHash} message=${errorMsg}`,
|
|
1351
|
+
null,
|
|
1352
|
+
);
|
|
1353
|
+
setThread(uwfAfter.varStore, threadId, markThreadSuspended(entry, role, errorMsg));
|
|
1354
|
+
return {
|
|
1355
|
+
workflow: workflowHash,
|
|
1356
|
+
thread: threadId,
|
|
1357
|
+
head: entry.head,
|
|
1358
|
+
status: "suspended",
|
|
1359
|
+
currentRole: role,
|
|
1360
|
+
suspendedRole: role,
|
|
1361
|
+
suspendMessage: errorMsg,
|
|
1362
|
+
done: false,
|
|
1363
|
+
background: null,
|
|
1364
|
+
error: { stepHash: replacedHash, message: errorMsg },
|
|
1365
|
+
};
|
|
1487
1366
|
}
|
|
1488
1367
|
|
|
1489
1368
|
// Update thread head to the replaced step. Status becomes idle (no moderator re-route).
|
|
@@ -1928,16 +1807,11 @@ async function cmdThreadStepOnce(
|
|
|
1928
1807
|
const { role, edgePrompt, effectiveCwd } = targetOrOutput;
|
|
1929
1808
|
|
|
1930
1809
|
const config = await loadWorkflowConfig(storageRoot);
|
|
1931
|
-
const agent = resolveAgentConfig(config, workflow, role, agentOverride);
|
|
1932
|
-
|
|
1933
|
-
plog.log(PL_AGENT_SPAWN, `spawning agent command=${agent.command}`, {
|
|
1934
|
-
args: [...agent.args, threadId, role].join(" "),
|
|
1935
|
-
});
|
|
1936
1810
|
|
|
1937
1811
|
loadDotenv({ path: getEnvPath(storageRoot) });
|
|
1938
1812
|
|
|
1939
|
-
// Wrap
|
|
1940
|
-
// (
|
|
1813
|
+
// Wrap broker execution in a try-catch: when the broker raises a fatal
|
|
1814
|
+
// error (HTTP failure, schema validation crash, etc.), failStep throws
|
|
1941
1815
|
// StepFailureError. We catch it to persist suspended state before re-throwing
|
|
1942
1816
|
// so the CLI still exits non-zero.
|
|
1943
1817
|
try {
|
|
@@ -1950,7 +1824,10 @@ async function cmdThreadStepOnce(
|
|
|
1950
1824
|
role,
|
|
1951
1825
|
edgePrompt,
|
|
1952
1826
|
effectiveCwd,
|
|
1953
|
-
|
|
1827
|
+
uwf,
|
|
1828
|
+
config,
|
|
1829
|
+
chain.startHash,
|
|
1830
|
+
agentOverride,
|
|
1954
1831
|
plog,
|
|
1955
1832
|
);
|
|
1956
1833
|
} catch (e) {
|
|
@@ -1965,8 +1842,13 @@ async function cmdThreadStepOnce(
|
|
|
1965
1842
|
}
|
|
1966
1843
|
|
|
1967
1844
|
/**
|
|
1968
|
-
* Execute the
|
|
1845
|
+
* Execute the broker step and process the result. Separated from cmdThreadStepOnce
|
|
1969
1846
|
* so that fatal failures (StepFailureError) can be caught and handled by the caller.
|
|
1847
|
+
*
|
|
1848
|
+
* Phase 3 (#380) — drives the broker via `executeBrokerStep` rather than the
|
|
1849
|
+
* legacy `spawnAgent` path. The broker writes the StepNode (including the
|
|
1850
|
+
* frontmatter retry chain) directly into CAS; the CLI only advances the head
|
|
1851
|
+
* pointer afterwards.
|
|
1970
1852
|
*/
|
|
1971
1853
|
async function executeAndProcessAgentStep(
|
|
1972
1854
|
storageRoot: string,
|
|
@@ -1977,30 +1859,43 @@ async function executeAndProcessAgentStep(
|
|
|
1977
1859
|
role: string,
|
|
1978
1860
|
edgePrompt: string,
|
|
1979
1861
|
effectiveCwd: string,
|
|
1980
|
-
|
|
1862
|
+
uwf: UwfStore,
|
|
1863
|
+
config: WorkflowConfig,
|
|
1864
|
+
startHash: CasRef,
|
|
1865
|
+
agentOverride: string | null,
|
|
1981
1866
|
plog: ProcessLogger,
|
|
1982
1867
|
): Promise<StepOutput> {
|
|
1983
|
-
const
|
|
1984
|
-
|
|
1868
|
+
const result = await executeBrokerStep({
|
|
1869
|
+
storageRoot,
|
|
1870
|
+
uwf,
|
|
1871
|
+
config,
|
|
1872
|
+
workflow,
|
|
1873
|
+
threadId,
|
|
1874
|
+
role,
|
|
1875
|
+
edgePrompt,
|
|
1876
|
+
effectiveCwd,
|
|
1877
|
+
startHash,
|
|
1878
|
+
prevHash: headHash === startHash ? null : headHash,
|
|
1879
|
+
agentOverride,
|
|
1880
|
+
previousAttempts: null,
|
|
1881
|
+
plog,
|
|
1882
|
+
});
|
|
1985
1883
|
|
|
1986
|
-
|
|
1884
|
+
const newHead = result.stepHash;
|
|
1885
|
+
plog.log(PL_AGENT_DONE, `broker returned head=${newHead}`, null);
|
|
1987
1886
|
|
|
1988
1887
|
const uwfAfter = await createUwfStore(storageRoot);
|
|
1989
|
-
const newNode = uwfAfter.store.cas.get(newHead);
|
|
1990
|
-
if (newNode === null || newNode.type !== uwfAfter.schemas.stepNode) {
|
|
1991
|
-
failStep(plog, `agent returned hash that is not a StepNode: ${newHead}`);
|
|
1992
|
-
}
|
|
1993
1888
|
|
|
1994
|
-
// Recoverable failure:
|
|
1889
|
+
// Recoverable failure: broker persisted a failed StepNode (e.g. frontmatter
|
|
1995
1890
|
// validation exhausted retries) but the engine MUST NOT advance head. The
|
|
1996
1891
|
// moderator graph is also untouched — the same role will be replayed on the
|
|
1997
1892
|
// next exec (until eventual success records `previousAttempts` linking the
|
|
1998
1893
|
// failed step hashes).
|
|
1999
|
-
if (
|
|
2000
|
-
const errorMsg =
|
|
1894
|
+
if (result.isError === true) {
|
|
1895
|
+
const errorMsg = result.errorMessage ?? "broker reported error";
|
|
2001
1896
|
plog.log(
|
|
2002
1897
|
PL_AGENT_ERROR,
|
|
2003
|
-
`
|
|
1898
|
+
`broker reported recoverable failure stepHash=${newHead} message=${errorMsg}`,
|
|
2004
1899
|
null,
|
|
2005
1900
|
);
|
|
2006
1901
|
|
package/dist/.build-fingerprint
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
8f6475d4155e0628083a10a7c1b3bc83482b887cdcfd2a704a00492f70553d87
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter-json-roundtrip.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adapter-json-roundtrip.test.ts"],"names":[],"mappings":""}
|