@gr8ful/spf 0.15.0 → 0.17.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 +15 -5
- package/assets/skill/references/config.md +9 -5
- package/assets/skill/references/observability.md +57 -12
- package/assets/templates/ts-opencode.spf.config.yaml +54 -0
- package/dist/chains/index.js +1 -1
- package/dist/chains/simple_sdlc.d.ts +2 -2
- package/dist/chains/simple_sdlc.js +13 -13
- package/dist/chains/steps.d.ts +2 -2
- package/dist/chains/steps.js +35 -19
- package/dist/cli/commands/abort.d.ts +1 -1
- package/dist/cli/commands/abort.js +30 -3
- package/dist/cli/commands/doctor.js +109 -8
- package/dist/cli/commands/estimate.js +3 -3
- package/dist/cli/commands/events.js +4 -4
- package/dist/cli/commands/fanout.js +93 -21
- package/dist/cli/commands/loop.js +31 -32
- package/dist/cli/commands/migrate.js +8 -1
- package/dist/cli/commands/phases.js +2 -2
- package/dist/cli/commands/sessions.js +2 -2
- package/dist/cli/commands/trace.d.ts +28 -8
- package/dist/cli/commands/trace.js +28 -15
- package/dist/cli/commands/ui.js +15 -5
- package/dist/cli/commands/watch.js +27 -27
- package/dist/cli/index.js +3 -1
- package/dist/cli/interview.d.ts +1 -0
- package/dist/cli/interview.js +86 -4
- package/dist/core/agent_opencode.d.ts +247 -0
- package/dist/core/agent_opencode.js +590 -0
- package/dist/core/agents.d.ts +12 -12
- package/dist/core/agents.js +113 -46
- package/dist/core/console.d.ts +12 -12
- package/dist/core/console.js +25 -25
- package/dist/core/data_types.d.ts +126 -12
- package/dist/core/data_types.js +101 -4
- package/dist/core/fanout.d.ts +1 -1
- package/dist/core/fanout.js +1 -1
- package/dist/core/gates.js +14 -1
- package/dist/core/paths.d.ts +41 -4
- package/dist/core/paths.js +32 -3
- package/dist/core/quality.d.ts +7 -7
- package/dist/core/quality.js +16 -10
- package/dist/core/runner.d.ts +9 -3
- package/dist/core/runner.js +39 -27
- package/dist/core/session.d.ts +2 -2
- package/dist/core/session.js +39 -18
- package/dist/core/sqlite.d.ts +14 -7
- package/dist/core/sqlite.js +14 -7
- package/dist/core/trace_db.d.ts +118 -0
- package/dist/core/trace_db.js +278 -0
- package/dist/core/tracer.d.ts +64 -34
- package/dist/core/tracer.js +141 -69
- package/dist/core/watch.d.ts +4 -4
- package/dist/core/watch.js +2 -2
- package/dist/ui/server/app.js +10 -10
- package/dist/ui/server/db.d.ts +89 -21
- package/dist/ui/server/db.js +235 -99
- package/dist/ui/server/serve.d.ts +5 -1
- package/dist/ui/server/serve.js +4 -5
- package/package.json +1 -1
- package/web/assets/index-CQ3k1Y1-.css +1 -0
- package/web/assets/index-CU8tom6S.js +21 -0
- package/web/index.html +2 -2
- package/web/assets/index-CRujNW-1.js +0 -11
- package/web/assets/index-Cto6nuQL.css +0 -1
package/dist/core/agents.js
CHANGED
|
@@ -13,6 +13,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
13
13
|
import * as v from "valibot";
|
|
14
14
|
import * as agentCc from "./agent_cc.js";
|
|
15
15
|
import * as agentFlue from "./agent_flue.js";
|
|
16
|
+
import * as agentOpencode from "./agent_opencode.js";
|
|
16
17
|
import * as paths from "./paths.js";
|
|
17
18
|
import * as permissions from "./permissions.js";
|
|
18
19
|
import * as prompts from "./prompts.js";
|
|
@@ -309,11 +310,13 @@ export function validate(cfg, required, requiredSuites = [], cwd) {
|
|
|
309
310
|
problems.push(`agent ${JSON.stringify(name)}: ${label} ${error.message}`);
|
|
310
311
|
}
|
|
311
312
|
}
|
|
312
|
-
// Model shape depends on the backend: Flue
|
|
313
|
-
// catalog to check against
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
|
|
313
|
+
// Model shape depends on the backend: Flue and opencode both need
|
|
314
|
+
// provider/model-id (no catalog to check against for either — opencode's
|
|
315
|
+
// own provider set differs from Flue's — only the static shape, so
|
|
316
|
+
// `agentFlue.resolveModel`'s shape-only check is reused as-is); claude_code
|
|
317
|
+
// takes its own bare alias/full-name vocabulary (see agent_cc.ts), so only
|
|
318
|
+
// a non-empty check applies.
|
|
319
|
+
if (agent.coding_agent === "flue" || agent.coding_agent === "opencode") {
|
|
317
320
|
try {
|
|
318
321
|
agentFlue.resolveModel(agent.model);
|
|
319
322
|
}
|
|
@@ -333,7 +336,7 @@ export function validate(cfg, required, requiredSuites = [], cwd) {
|
|
|
333
336
|
problems.push(`agent ${JSON.stringify(name)}: harness_engineering is not supported (${JSON.stringify(agent.harness_engineering)}) — ` +
|
|
334
337
|
`use useSubagent()/defineSkill() (flue) or an MCP server/plugin (claude_code) in a custom tool instead`);
|
|
335
338
|
}
|
|
336
|
-
const isKnownToolName = agent.coding_agent === "claude_code" ? agentCc.isKnownToolName : agentFlue.isKnownToolName;
|
|
339
|
+
const isKnownToolName = agent.coding_agent === "claude_code" ? agentCc.isKnownToolName : agent.coding_agent === "opencode" ? agentOpencode.isKnownToolName : agentFlue.isKnownToolName;
|
|
337
340
|
for (const toolName of agent.tools ?? []) {
|
|
338
341
|
if (!isKnownToolName(toolName)) {
|
|
339
342
|
problems.push(`agent ${JSON.stringify(name)}: unknown tool ${JSON.stringify(toolName)} — known: read, write, edit, bash, grep, glob, find (alias for glob), ls (dropped, covered by bash/glob)`);
|
|
@@ -360,7 +363,7 @@ export function validate(cfg, required, requiredSuites = [], cwd) {
|
|
|
360
363
|
// declared backend — the same branch this function already runs for
|
|
361
364
|
// agent.model, just keyed off the tier's coding_agent instead.
|
|
362
365
|
for (const tier of cfg.tiering.tiers) {
|
|
363
|
-
if (tier.coding_agent === "flue") {
|
|
366
|
+
if (tier.coding_agent === "flue" || tier.coding_agent === "opencode") {
|
|
364
367
|
try {
|
|
365
368
|
agentFlue.resolveModel(tier.model);
|
|
366
369
|
}
|
|
@@ -468,12 +471,13 @@ export function validateSandboxConfig(cfg, agent) {
|
|
|
468
471
|
const problems = [];
|
|
469
472
|
const sb = cfg.sandbox;
|
|
470
473
|
const backend = agent.sandbox ?? sb.backend;
|
|
471
|
-
// claude_code
|
|
472
|
-
// sandbox seam at all —
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
474
|
+
// claude_code and opencode both spawn a host process (agent_cc.ts's /
|
|
475
|
+
// agent_opencode.ts's own spawn()) with no sandbox seam at all —
|
|
476
|
+
// sandboxing either would mean running the CLI INSIDE the container, a
|
|
477
|
+
// different feature, not this config flag.
|
|
478
|
+
if ((agent.coding_agent === "claude_code" || agent.coding_agent === "opencode") && backend !== "local") {
|
|
479
|
+
problems.push(`agent ${JSON.stringify(agent.name)}: coding_agent ${JSON.stringify(agent.coding_agent)} cannot use sandbox backend ${JSON.stringify(backend)} — ` +
|
|
480
|
+
`${agent.coding_agent} always spawns a host process with no sandbox seam; set agent.sandbox: local (or sandbox.backend: local) for this agent`);
|
|
477
481
|
}
|
|
478
482
|
if (backend === "opensandbox") {
|
|
479
483
|
if (!sb.opensandbox.base_url.trim()) {
|
|
@@ -738,13 +742,15 @@ export async function execute(run, phase, call) {
|
|
|
738
742
|
// translation need the spec baked into the rendered prompt text.
|
|
739
743
|
const spec = sandboxSpecFor(run, agent);
|
|
740
744
|
if (spec) {
|
|
741
|
-
sandbox.registerRunLog(spec.adw_id, (e) => run.tracer
|
|
745
|
+
sandbox.registerRunLog(spec.adw_id, (e) => void run.tracer
|
|
746
|
+
.event(makeEventRecord({
|
|
742
747
|
adw_id: run.adw_id,
|
|
743
748
|
phase_id: phase.phase_id,
|
|
744
749
|
type: "log",
|
|
745
750
|
name: "sandbox",
|
|
746
751
|
payload: { level: e.level, msg: e.msg, ...(e.data ? { data: e.data } : {}) },
|
|
747
|
-
}))
|
|
752
|
+
}))
|
|
753
|
+
.catch(() => { }));
|
|
748
754
|
}
|
|
749
755
|
const variables = {
|
|
750
756
|
prompt: call.prompt,
|
|
@@ -762,8 +768,18 @@ export async function execute(run, phase, call) {
|
|
|
762
768
|
const userText = prompts.render(paths.resolvePromptRef(run, agent.prompt_engineering.user), variables);
|
|
763
769
|
prompts.save(path.join(agentDir, "prompts"), "system.md", systemText);
|
|
764
770
|
prompts.save(path.join(agentDir, "prompts"), "user.md", userText);
|
|
765
|
-
|
|
766
|
-
|
|
771
|
+
// `sessionId` is reassigned by `send()` below (see its comment), ONLY for
|
|
772
|
+
// opencode, whenever its result carries the real captured id — its
|
|
773
|
+
// first-contact call starts from a cosmetic placeholder (see
|
|
774
|
+
// agent_opencode.ts's `pendingSessionLabel()`) and must swap in the real
|
|
775
|
+
// one before any later send() in this same phase (a JSON-repair retry, a
|
|
776
|
+
// gate correction). Deliberately scoped to opencode only, not a blanket
|
|
777
|
+
// reassignment from every backend's result — see `send()`'s comment for
|
|
778
|
+
// why that would NOT be a no-op for claude_code.
|
|
779
|
+
const initialSession = agentSessionId(run, agent);
|
|
780
|
+
const isNewSession = initialSession.is_new;
|
|
781
|
+
let sessionId = initialSession.session_id;
|
|
782
|
+
await run.tracer.event(makeEventRecord({
|
|
767
783
|
adw_id: run.adw_id,
|
|
768
784
|
phase_id: phase.phase_id,
|
|
769
785
|
type: "agent_start",
|
|
@@ -779,7 +795,7 @@ export async function execute(run, phase, call) {
|
|
|
779
795
|
harness_engineering: agent.harness_engineering,
|
|
780
796
|
},
|
|
781
797
|
}));
|
|
782
|
-
run.console.agentStarted(agent.name, agent.model, sessionId);
|
|
798
|
+
await run.console.agentStarted(agent.name, agent.model, sessionId);
|
|
783
799
|
// Parse retries and gate corrections re-enter the SAME Flue conversation,
|
|
784
800
|
// so the last send is the one whose context occupancy is current — while
|
|
785
801
|
// spend is the opposite: every send costs, so usage accumulates across all.
|
|
@@ -808,17 +824,46 @@ export async function execute(run, phase, call) {
|
|
|
808
824
|
sandbox: spec,
|
|
809
825
|
};
|
|
810
826
|
const forward = eventForwarder(run, phase, agent.name, agent.coding_agent);
|
|
811
|
-
|
|
812
|
-
|
|
827
|
+
// Best-effort, fire-and-forget: these fire from a plain process-lifecycle
|
|
828
|
+
// callback (agent_cc.ts/agent_flue.ts's onSpawn/onExit hooks), not from
|
|
829
|
+
// this function's own async control flow, so there is nothing useful to
|
|
830
|
+
// await here — same swallow-and-log discipline as tracer.ts's otel fanOut.
|
|
831
|
+
const onSpawn = (pid) => void run.tracer.processStart(run.adw_id, "agent", agent.name, pid, `${agent.coding_agent} ${agent.name} ${agent.model}`).catch(() => { });
|
|
832
|
+
const onExit = (pid) => void run.tracer.processEnd(run.adw_id, pid).catch(() => { });
|
|
813
833
|
if (spec)
|
|
814
834
|
await sandbox.reconcileWorkspace(spec);
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
835
|
+
let result;
|
|
836
|
+
if (agent.coding_agent === "claude_code") {
|
|
837
|
+
result = await agentCc.run(request, forward, onSpawn, onExit);
|
|
838
|
+
}
|
|
839
|
+
else if (agent.coding_agent === "opencode") {
|
|
840
|
+
result = await agentOpencode.run(request, forward, onSpawn, onExit);
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
result = await agentFlue.run(request, forward, onSpawn, onExit);
|
|
844
|
+
}
|
|
818
845
|
// SPEND IS RECORDED BEFORE THE EXTRACT CAN THROW: a failed extract must
|
|
819
846
|
// not also lose this call's tokens/cost off the Run's ledger.
|
|
820
|
-
run.addUsage(result.tokens, result.cost);
|
|
847
|
+
await run.addUsage(result.tokens, result.cost);
|
|
821
848
|
spent.merge(result.usage);
|
|
849
|
+
// opencode-ONLY: every subsequent send() in THIS phase (JSON-repair
|
|
850
|
+
// retries, gate corrections) must target the real captured session, not
|
|
851
|
+
// the placeholder this phase started with (see `initialSession`'s
|
|
852
|
+
// comment above and agent_opencode.ts's session-id lifecycle notes).
|
|
853
|
+
// Deliberately NOT applied to flue/claude_code — agent_cc.ts returns
|
|
854
|
+
// `final.session_id ?? request.session_id`, which PREFERS Claude Code's
|
|
855
|
+
// own reported id over the requested one when the two differ, so
|
|
856
|
+
// reassigning unconditionally here would let a claude_code run's
|
|
857
|
+
// recorded session id (agent_map.json, the agent_sessions trace table)
|
|
858
|
+
// drift from the one actually passed to `--session-id`/`--resume` —
|
|
859
|
+
// scoping this to opencode keeps flue/claude_code byte-identical to
|
|
860
|
+
// before this backend existed. `result.session_id` is also guarded
|
|
861
|
+
// against a falsy value here: a still-unresolved [SOURCE] NDJSON field
|
|
862
|
+
// name in agent_opencode.ts's `run()` should surface as a thrown error
|
|
863
|
+
// there (see its "sawStop but no captured session id" check), never as
|
|
864
|
+
// a silently-persisted empty/placeholder id.
|
|
865
|
+
if (agent.coding_agent === "opencode" && result.session_id)
|
|
866
|
+
sessionId = result.session_id;
|
|
822
867
|
latest = result;
|
|
823
868
|
if (spec)
|
|
824
869
|
await sandbox.extractWorkspace(spec);
|
|
@@ -837,15 +882,15 @@ export async function execute(run, phase, call) {
|
|
|
837
882
|
for (const gate of call.gates || []) {
|
|
838
883
|
const report = asReport(gate(envelope, run));
|
|
839
884
|
const found = report.violations;
|
|
840
|
-
run.tracer.gateRow(phase, gate.name, report, gateAttempt);
|
|
841
|
-
run.tracer.event(makeEventRecord({
|
|
885
|
+
await run.tracer.gateRow(phase, gate.name, report, gateAttempt);
|
|
886
|
+
await run.tracer.event(makeEventRecord({
|
|
842
887
|
adw_id: run.adw_id,
|
|
843
888
|
phase_id: phase.phase_id,
|
|
844
889
|
type: found.length > 0 ? "gate_fail" : "gate_pass",
|
|
845
890
|
name: gate.name,
|
|
846
891
|
payload: { attempt: gateAttempt, violations: found, checks: report.checks },
|
|
847
892
|
}));
|
|
848
|
-
run.console.gateResult(gate.name, report);
|
|
893
|
+
await run.console.gateResult(gate.name, report);
|
|
849
894
|
violations.push(...found);
|
|
850
895
|
}
|
|
851
896
|
if (violations.length === 0)
|
|
@@ -854,7 +899,7 @@ export async function execute(run, phase, call) {
|
|
|
854
899
|
throw new GateFailure(`${agent.name} failed gates after ${gateAttempt} attempt(s):\n- ` + violations.join("\n- "));
|
|
855
900
|
}
|
|
856
901
|
phase.attempt = gateAttempt;
|
|
857
|
-
run.console.retry(agent.name, gateAttempt, phase.params.retries, `${violations.length} gate violation(s)`);
|
|
902
|
+
await run.console.retry(agent.name, gateAttempt, phase.params.retries, `${violations.length} gate violation(s)`);
|
|
858
903
|
const correction = "Your previous response failed validation:\n- " +
|
|
859
904
|
violations.join("\n- ") +
|
|
860
905
|
"\n\nFix these problems, then re-emit ONLY your Report JSON.";
|
|
@@ -870,7 +915,7 @@ export async function execute(run, phase, call) {
|
|
|
870
915
|
touched = permissions.enforce(run, phase, agent, treeBefore);
|
|
871
916
|
}
|
|
872
917
|
catch (breach) {
|
|
873
|
-
run.tracer.event(makeEventRecord({
|
|
918
|
+
await run.tracer.event(makeEventRecord({
|
|
874
919
|
adw_id: run.adw_id,
|
|
875
920
|
phase_id: phase.phase_id,
|
|
876
921
|
type: "error",
|
|
@@ -885,7 +930,7 @@ export async function execute(run, phase, call) {
|
|
|
885
930
|
throw breach;
|
|
886
931
|
}
|
|
887
932
|
if (touched.length > 0) {
|
|
888
|
-
run.tracer.event(makeEventRecord({
|
|
933
|
+
await run.tracer.event(makeEventRecord({
|
|
889
934
|
adw_id: run.adw_id,
|
|
890
935
|
phase_id: phase.phase_id,
|
|
891
936
|
type: "log",
|
|
@@ -893,19 +938,19 @@ export async function execute(run, phase, call) {
|
|
|
893
938
|
payload: { agent: agent.name, paths: touched },
|
|
894
939
|
}));
|
|
895
940
|
}
|
|
896
|
-
persistEnvelope(run, phase, agent.name, call, envelope, parsed.attempt, true);
|
|
897
|
-
run.console.envelopeSummary(envelope, call.output_type.name);
|
|
941
|
+
await persistEnvelope(run, phase, agent.name, call, envelope, parsed.attempt, true);
|
|
942
|
+
await run.console.envelopeSummary(envelope, call.output_type.name);
|
|
898
943
|
const context = latest ?? result;
|
|
899
|
-
run.tracer.agentSessionRow(run.adw_id, agent, sessionId, context.context_tokens, context.context_window);
|
|
944
|
+
await run.tracer.agentSessionRow(run.adw_id, agent, sessionId, context.context_tokens, context.context_window);
|
|
900
945
|
run.saveAgentMap(agent.name, { session_id: sessionId, model: agent.model, coding_agent: agent.coding_agent });
|
|
901
|
-
run.tracer.event(makeEventRecord({
|
|
946
|
+
await run.tracer.event(makeEventRecord({
|
|
902
947
|
adw_id: run.adw_id,
|
|
903
948
|
phase_id: phase.phase_id,
|
|
904
949
|
type: "handoff",
|
|
905
950
|
name: agent.name,
|
|
906
951
|
payload: { artifacts: envelope.artifacts, summary: envelope.summary },
|
|
907
952
|
}));
|
|
908
|
-
run.tracer.event(makeEventRecord({
|
|
953
|
+
await run.tracer.event(makeEventRecord({
|
|
909
954
|
adw_id: run.adw_id,
|
|
910
955
|
phase_id: phase.phase_id,
|
|
911
956
|
type: "agent_end",
|
|
@@ -919,7 +964,7 @@ export async function execute(run, phase, call) {
|
|
|
919
964
|
context_window: context.context_window,
|
|
920
965
|
},
|
|
921
966
|
}));
|
|
922
|
-
run.console.agentFinished(agent.name, spent.total_tokens, spent.total_cost);
|
|
967
|
+
await run.console.agentFinished(agent.name, spent.total_tokens, spent.total_cost);
|
|
923
968
|
if (envelope.status !== "success") {
|
|
924
969
|
throw new Error(`${agent.name} reported status=${JSON.stringify(envelope.status)}: ${envelope.summary}`);
|
|
925
970
|
}
|
|
@@ -943,9 +988,26 @@ function asReport(result) {
|
|
|
943
988
|
*/
|
|
944
989
|
function agentSessionId(run, agent) {
|
|
945
990
|
const entry = run.agent_map[agent.name];
|
|
946
|
-
|
|
991
|
+
// A session handle from one backend is meaningless to another — flue and
|
|
992
|
+
// opencode share the identical provider/model-id vocabulary, so a
|
|
993
|
+
// coding_agent flip on an existing agent_map.json entry can otherwise
|
|
994
|
+
// leave `entry.model === agent.model` true while the stored session_id's
|
|
995
|
+
// SHAPE no longer matches the new backend (e.g. a flue `spf-...` id handed
|
|
996
|
+
// to opencode's `--session`, which requires its own `ses_<ULID>` shape).
|
|
997
|
+
if (entry && entry.model === agent.model && entry.coding_agent === agent.coding_agent) {
|
|
947
998
|
return { session_id: entry.session_id, is_new: false }; // rejoin the existing context window
|
|
948
|
-
|
|
999
|
+
}
|
|
1000
|
+
// opencode assigns its OWN real session id on first contact (never
|
|
1001
|
+
// client-choosable — see agent_opencode.ts's module doc comment); what's
|
|
1002
|
+
// minted here is a purely cosmetic placeholder for the pre-call trace
|
|
1003
|
+
// event / console log, replaced by the real captured id the moment
|
|
1004
|
+
// send() gets its first result back (see execute()'s `sessionId` handling
|
|
1005
|
+
// below).
|
|
1006
|
+
const session_id = agent.coding_agent === "claude_code"
|
|
1007
|
+
? agentCc.newSessionId()
|
|
1008
|
+
: agent.coding_agent === "opencode"
|
|
1009
|
+
? agentOpencode.pendingSessionLabel()
|
|
1010
|
+
: `spf-${run.adw_id}-${agent.name}-${newId(4)}`;
|
|
949
1011
|
return { session_id, is_new: true };
|
|
950
1012
|
}
|
|
951
1013
|
/**
|
|
@@ -955,15 +1017,19 @@ function agentSessionId(run, agent) {
|
|
|
955
1017
|
* this stays a plain pass-through regardless of which one it's given.
|
|
956
1018
|
*/
|
|
957
1019
|
function eventForwarder(run, phase, agentName, codingAgent) {
|
|
958
|
-
const tracker = codingAgent === "claude_code" ? new agentCc.CcToolCallTracker() : new agentFlue.ToolCallTracker();
|
|
1020
|
+
const tracker = codingAgent === "claude_code" ? new agentCc.CcToolCallTracker() : codingAgent === "opencode" ? new agentOpencode.OcToolCallTracker() : new agentFlue.ToolCallTracker();
|
|
959
1021
|
return (chunk) => {
|
|
960
1022
|
const record = tracker.observe(chunk);
|
|
961
1023
|
if (record === null)
|
|
962
1024
|
return;
|
|
963
1025
|
// The call's span rides the columns; duration_ms stays in the payload as
|
|
964
|
-
// Flue's own authoritative number.
|
|
1026
|
+
// Flue's own authoritative number. Fire-and-forget, same reasoning as
|
|
1027
|
+
// onSpawn/onExit above: this callback fires from a synchronous stream
|
|
1028
|
+
// event handler (agent_cc.ts's readline `line` event / agent_flue.ts's
|
|
1029
|
+
// chunk stream), never from this function's own async control flow.
|
|
965
1030
|
const { label, started_at, ended_at, ...rest } = record;
|
|
966
|
-
run.tracer
|
|
1031
|
+
void run.tracer
|
|
1032
|
+
.event(makeEventRecord({
|
|
967
1033
|
adw_id: run.adw_id,
|
|
968
1034
|
phase_id: phase.phase_id,
|
|
969
1035
|
type: "tool_call",
|
|
@@ -971,7 +1037,8 @@ function eventForwarder(run, phase, agentName, codingAgent) {
|
|
|
971
1037
|
started_at: started_at ?? null,
|
|
972
1038
|
ended_at: ended_at ?? null,
|
|
973
1039
|
payload: { ...rest, agent: agentName },
|
|
974
|
-
}))
|
|
1040
|
+
}))
|
|
1041
|
+
.catch(() => { });
|
|
975
1042
|
};
|
|
976
1043
|
}
|
|
977
1044
|
function extractJson(text) {
|
|
@@ -1016,11 +1083,11 @@ async function parseWithRetries(run, phase, call, result, send, spec) {
|
|
|
1016
1083
|
}
|
|
1017
1084
|
catch (error) {
|
|
1018
1085
|
const message = describeParseError(error);
|
|
1019
|
-
persistEnvelope(run, phase, phase.params.owner, call, null, attempt, false, current.text);
|
|
1086
|
+
await persistEnvelope(run, phase, phase.params.owner, call, null, attempt, false, current.text);
|
|
1020
1087
|
if (attempt > JSON_FIX_ATTEMPTS) {
|
|
1021
1088
|
throw new Error(`${phase.params.owner} never produced valid ${call.output_type.name} JSON: ${message}`);
|
|
1022
1089
|
}
|
|
1023
|
-
run.console.retry(phase.params.owner, attempt, JSON_FIX_ATTEMPTS, `invalid ${call.output_type.name} JSON: ${message}`);
|
|
1090
|
+
await run.console.retry(phase.params.owner, attempt, JSON_FIX_ATTEMPTS, `invalid ${call.output_type.name} JSON: ${message}`);
|
|
1024
1091
|
const fields = call.output_type.fields.join(", ");
|
|
1025
1092
|
current = await send(`Your response was not valid JSON for the required structure ` +
|
|
1026
1093
|
`(${message}). Respond again with ONLY a JSON object with these ` +
|
|
@@ -1029,9 +1096,9 @@ async function parseWithRetries(run, phase, call, result, send, spec) {
|
|
|
1029
1096
|
}
|
|
1030
1097
|
throw new Error("unreachable");
|
|
1031
1098
|
}
|
|
1032
|
-
function persistEnvelope(run, phase, agentName, call, envelope, attempt, valid, raw = "") {
|
|
1099
|
+
async function persistEnvelope(run, phase, agentName, call, envelope, attempt, valid, raw = "") {
|
|
1033
1100
|
const payloadJson = envelope ? JSON.stringify(envelope, null, 2) : JSON.stringify({ raw: raw.slice(-2000) });
|
|
1034
|
-
run.tracer.envelopeRow(phase, agentName, call.output_type.name, payloadJson, valid, attempt);
|
|
1101
|
+
await run.tracer.envelopeRow(phase, agentName, call.output_type.name, payloadJson, valid, attempt);
|
|
1035
1102
|
if (envelope) {
|
|
1036
1103
|
const record = {
|
|
1037
1104
|
agent_name: agentName,
|
package/dist/core/console.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ interface Notifier {
|
|
|
13
13
|
}
|
|
14
14
|
export declare function paint(style: string, text: string): string;
|
|
15
15
|
interface Tracer {
|
|
16
|
-
event(record: EventRecord): string
|
|
16
|
+
event(record: EventRecord): Promise<string>;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* A run-scoped hook for a live TTY view (`cli/ui/run_dashboard.tsx`) to
|
|
@@ -70,19 +70,19 @@ export declare class Console {
|
|
|
70
70
|
/** See `RunObserver`'s own doc comment. `null` outside an interactive `cli/commands/run.ts` dispatch. */
|
|
71
71
|
observer?: RunObserver | null);
|
|
72
72
|
private emit;
|
|
73
|
-
sessionStarted(adwId: string, engineer: string): void
|
|
74
|
-
sessionFinished(ok: boolean, tokens: number, cost: number, dbPath: string): void
|
|
75
|
-
phaseStarted(phase: Phase): void
|
|
76
|
-
phaseEnded(phase: Phase, seconds: number): void
|
|
73
|
+
sessionStarted(adwId: string, engineer: string): Promise<void>;
|
|
74
|
+
sessionFinished(ok: boolean, tokens: number, cost: number, dbPath: string): Promise<void>;
|
|
75
|
+
phaseStarted(phase: Phase): Promise<void>;
|
|
76
|
+
phaseEnded(phase: Phase, seconds: number): Promise<void>;
|
|
77
77
|
/** Free-form detail inside the current phase — what `ph.log()` recorded. */
|
|
78
|
-
note(message: string): void
|
|
78
|
+
note(message: string): Promise<void>;
|
|
79
79
|
/** `Run.addUsage()`'s only hook into `Console` — the running total lives on `Run`, not here, so this just forwards it to the observer. No line prints for this on its own; the totals already show up in `sessionFinished`'s panel. */
|
|
80
|
-
notifyUsage(tokens: number, cost: number): void
|
|
81
|
-
agentStarted(name: string, model: string, sessionId: string): void
|
|
82
|
-
agentFinished(name: string, tokens: number, cost: number): void
|
|
83
|
-
retry(name: string, attempt: number, limit: number, reason: string): void
|
|
80
|
+
notifyUsage(tokens: number, cost: number): Promise<void>;
|
|
81
|
+
agentStarted(name: string, model: string, sessionId: string): Promise<void>;
|
|
82
|
+
agentFinished(name: string, tokens: number, cost: number): Promise<void>;
|
|
83
|
+
retry(name: string, attempt: number, limit: number, reason: string): Promise<void>;
|
|
84
84
|
/** A gate reports WHAT it checked, not just whether it passed. */
|
|
85
|
-
gateResult(name: string, report: GateReport): void
|
|
86
|
-
envelopeSummary(envelope: EnvelopeBase, typeName: string): void
|
|
85
|
+
gateResult(name: string, report: GateReport): Promise<void>;
|
|
86
|
+
envelopeSummary(envelope: EnvelopeBase, typeName: string): Promise<void>;
|
|
87
87
|
}
|
|
88
88
|
export {};
|
package/dist/core/console.js
CHANGED
|
@@ -77,9 +77,9 @@ export class Console {
|
|
|
77
77
|
this.observer = observer;
|
|
78
78
|
}
|
|
79
79
|
// ── the one helper: print AND trace, always together ────────────────────
|
|
80
|
-
emit(line, level = "info") {
|
|
80
|
+
async emit(line, level = "info") {
|
|
81
81
|
this.sink(line);
|
|
82
|
-
this.tracer.event(makeEventRecord({
|
|
82
|
+
await this.tracer.event(makeEventRecord({
|
|
83
83
|
adw_id: this.adwId,
|
|
84
84
|
phase_id: this.phaseId,
|
|
85
85
|
type: "log",
|
|
@@ -88,8 +88,8 @@ export class Console {
|
|
|
88
88
|
}));
|
|
89
89
|
}
|
|
90
90
|
// ── session ─────────────────────────────────────────────────────────────
|
|
91
|
-
sessionStarted(adwId, engineer) {
|
|
92
|
-
this.emit(`${paint("bold cyan", "adw_id:")} ${paint("bold", adwId)} ${paint("dim", "engineer")} ${engineer}`);
|
|
91
|
+
async sessionStarted(adwId, engineer) {
|
|
92
|
+
await this.emit(`${paint("bold cyan", "adw_id:")} ${paint("bold", adwId)} ${paint("dim", "engineer")} ${engineer}`);
|
|
93
93
|
this.notifier?.send({
|
|
94
94
|
kind: "run_started",
|
|
95
95
|
level: "info",
|
|
@@ -100,7 +100,7 @@ export class Console {
|
|
|
100
100
|
],
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
-
sessionFinished(ok, tokens, cost, dbPath) {
|
|
103
|
+
async sessionFinished(ok, tokens, cost, dbPath) {
|
|
104
104
|
if (this.finished)
|
|
105
105
|
return;
|
|
106
106
|
this.finished = true;
|
|
@@ -119,7 +119,7 @@ export class Console {
|
|
|
119
119
|
this.sink(rendered);
|
|
120
120
|
this.observer?.onSessionEnd?.(ok);
|
|
121
121
|
const plain = `session ${this.adwId} ${ok ? "success" : "fail"} · ${passed}/${this.results.length} phases · ${tokens.toLocaleString()} tokens · $${cost.toFixed(4)}`;
|
|
122
|
-
this.tracer.event(makeEventRecord({
|
|
122
|
+
await this.tracer.event(makeEventRecord({
|
|
123
123
|
adw_id: this.adwId,
|
|
124
124
|
phase_id: this.phaseId,
|
|
125
125
|
type: "log",
|
|
@@ -139,7 +139,7 @@ export class Console {
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
// ── phases ──────────────────────────────────────────────────────────────
|
|
142
|
-
phaseStarted(phase) {
|
|
142
|
+
async phaseStarted(phase) {
|
|
143
143
|
this.phaseId = phase.phase_id;
|
|
144
144
|
this.phaseName = phase.params.name;
|
|
145
145
|
const p = phase.params;
|
|
@@ -148,17 +148,17 @@ export class Console {
|
|
|
148
148
|
` ${paint(color, p.kind)} ${paint("dim", `· ${p.owner}`)}`;
|
|
149
149
|
if (p.description)
|
|
150
150
|
line += ` ${paint("dim", clip(p.description))}`;
|
|
151
|
-
this.emit(line);
|
|
151
|
+
await this.emit(line);
|
|
152
152
|
this.observer?.onPhaseStart?.(phase);
|
|
153
153
|
}
|
|
154
|
-
phaseEnded(phase, seconds) {
|
|
154
|
+
async phaseEnded(phase, seconds) {
|
|
155
155
|
const ok = phase.status === "success";
|
|
156
156
|
this.results.push(phase.status);
|
|
157
157
|
this.observer?.onPhaseEnd?.(phase, seconds);
|
|
158
158
|
let line = ` ${ok ? paint("green", "✓") : paint("red", "✗")} ${phase.params.name} ${paint("dim", `${seconds.toFixed(1)}s`)}`;
|
|
159
159
|
if (!ok && phase.error)
|
|
160
160
|
line += ` ${paint("red", clip(phase.error))}`;
|
|
161
|
-
this.emit(line, ok ? "info" : "error");
|
|
161
|
+
await this.emit(line, ok ? "info" : "error");
|
|
162
162
|
if (!ok) {
|
|
163
163
|
this.notifier?.send({
|
|
164
164
|
kind: "phase_failed",
|
|
@@ -176,22 +176,22 @@ export class Console {
|
|
|
176
176
|
this.phaseName = "";
|
|
177
177
|
}
|
|
178
178
|
/** Free-form detail inside the current phase — what `ph.log()` recorded. */
|
|
179
|
-
note(message) {
|
|
180
|
-
this.emit(` ${paint("dim", `· ${clip(message)}`)}`);
|
|
179
|
+
async note(message) {
|
|
180
|
+
await this.emit(` ${paint("dim", `· ${clip(message)}`)}`);
|
|
181
181
|
}
|
|
182
182
|
/** `Run.addUsage()`'s only hook into `Console` — the running total lives on `Run`, not here, so this just forwards it to the observer. No line prints for this on its own; the totals already show up in `sessionFinished`'s panel. */
|
|
183
|
-
notifyUsage(tokens, cost) {
|
|
183
|
+
async notifyUsage(tokens, cost) {
|
|
184
184
|
this.observer?.onUsage?.(tokens, cost);
|
|
185
185
|
}
|
|
186
186
|
// ── agents ──────────────────────────────────────────────────────────────
|
|
187
|
-
agentStarted(name, model, sessionId) {
|
|
188
|
-
this.emit(` ${paint("magenta", "▸")} ${name} ${paint("dim", model)} ${paint("dim", `session ${sessionId}`)}`);
|
|
187
|
+
async agentStarted(name, model, sessionId) {
|
|
188
|
+
await this.emit(` ${paint("magenta", "▸")} ${name} ${paint("dim", model)} ${paint("dim", `session ${sessionId}`)}`);
|
|
189
189
|
}
|
|
190
|
-
agentFinished(name, tokens, cost) {
|
|
191
|
-
this.emit(` ${paint("dim", `└ ${name} used ${tokens.toLocaleString()} tokens · $${cost.toFixed(4)}`)}`);
|
|
190
|
+
async agentFinished(name, tokens, cost) {
|
|
191
|
+
await this.emit(` ${paint("dim", `└ ${name} used ${tokens.toLocaleString()} tokens · $${cost.toFixed(4)}`)}`);
|
|
192
192
|
}
|
|
193
|
-
retry(name, attempt, limit, reason) {
|
|
194
|
-
this.emit(` ${paint("yellow", "⟳")} ${name} retry ${attempt}/${limit} ${paint("dim", `— same session · ${clip(reason)}`)}`, "warn");
|
|
193
|
+
async retry(name, attempt, limit, reason) {
|
|
194
|
+
await this.emit(` ${paint("yellow", "⟳")} ${name} retry ${attempt}/${limit} ${paint("dim", `— same session · ${clip(reason)}`)}`, "warn");
|
|
195
195
|
// info-level: routine self-healing, same as watch's own untracked
|
|
196
196
|
// orphan retries — visible under `events: all`, silent under `errors`.
|
|
197
197
|
this.notifier?.send({
|
|
@@ -204,25 +204,25 @@ export class Console {
|
|
|
204
204
|
}
|
|
205
205
|
// ── verification ────────────────────────────────────────────────────────
|
|
206
206
|
/** A gate reports WHAT it checked, not just whether it passed. */
|
|
207
|
-
gateResult(name, report) {
|
|
207
|
+
async gateResult(name, report) {
|
|
208
208
|
const ok = report.passed;
|
|
209
209
|
const mark = ok ? paint("green", "✓") : paint("red", "✗");
|
|
210
210
|
const summary = ok
|
|
211
211
|
? `${report.checks.length} checked`
|
|
212
212
|
: paint("red", `${report.violations.length} of ${report.checks.length} failed`);
|
|
213
|
-
this.emit(` ${mark} gate ${paint("dim", name)} ${paint("dim", summary)}`, ok ? "info" : "error");
|
|
213
|
+
await this.emit(` ${mark} gate ${paint("dim", name)} ${paint("dim", summary)}`, ok ? "info" : "error");
|
|
214
214
|
for (const check of report.checks) {
|
|
215
215
|
const style = check.ok ? "dim" : "dim red";
|
|
216
216
|
const detail = check.note ? ` — ${clip(check.note)}` : "";
|
|
217
|
-
this.emit(` ${paint(style, `${check.ok ? "·" : "✗"} ${clip(check.item)}${detail}`)}`, check.ok ? "info" : "error");
|
|
217
|
+
await this.emit(` ${paint(style, `${check.ok ? "·" : "✗"} ${clip(check.item)}${detail}`)}`, check.ok ? "info" : "error");
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
-
envelopeSummary(envelope, typeName) {
|
|
220
|
+
async envelopeSummary(envelope, typeName) {
|
|
221
221
|
const ok = envelope.status === "success";
|
|
222
222
|
const line = ` ${ok ? paint("green", "✓") : paint("red", "✗")} ${typeName} ${paint("dim", clip(envelope.summary))}`;
|
|
223
|
-
this.emit(line, ok ? "info" : "error");
|
|
223
|
+
await this.emit(line, ok ? "info" : "error");
|
|
224
224
|
if (envelope.artifacts.length > 0) {
|
|
225
|
-
this.emit(` ${paint("dim", `artifacts: ${clip(envelope.artifacts.join(", "))}`)}`);
|
|
225
|
+
await this.emit(` ${paint("dim", `artifacts: ${clip(envelope.artifacts.join(", "))}`)}`);
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
}
|