@sapiom/harness 0.1.4 → 0.1.6
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/CHANGELOG.md +74 -0
- package/dist/cli/bin.js +24 -7
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/consent.js +1 -1
- package/dist/core/adapters/adapter.d.ts +2 -2
- package/dist/core/api-key-provider.d.ts +79 -0
- package/dist/core/api-key-provider.d.ts.map +1 -0
- package/dist/core/api-key-provider.js +91 -0
- package/dist/core/api-key-provider.js.map +1 -0
- package/dist/core/canvas-run-state.d.ts +99 -0
- package/dist/core/canvas-run-state.d.ts.map +1 -0
- package/dist/core/canvas-run-state.js +191 -0
- package/dist/core/canvas-run-state.js.map +1 -0
- package/dist/core/canvas-svg.d.ts.map +1 -1
- package/dist/core/canvas-svg.js +28 -8
- package/dist/core/canvas-svg.js.map +1 -1
- package/dist/core/canvas-template.d.ts +4 -3
- package/dist/core/canvas-template.d.ts.map +1 -1
- package/dist/core/canvas-template.js +75 -3
- package/dist/core/canvas-template.js.map +1 -1
- package/dist/core/definition-slug-resolver.d.ts +46 -0
- package/dist/core/definition-slug-resolver.d.ts.map +1 -0
- package/dist/core/definition-slug-resolver.js +110 -0
- package/dist/core/definition-slug-resolver.js.map +1 -0
- package/dist/core/execution-detector.d.ts +59 -0
- package/dist/core/execution-detector.d.ts.map +1 -0
- package/dist/core/execution-detector.js +110 -0
- package/dist/core/execution-detector.js.map +1 -0
- package/dist/core/inject/mcp-config.d.ts.map +1 -1
- package/dist/core/inject/mcp-config.js +9 -1
- package/dist/core/inject/mcp-config.js.map +1 -1
- package/dist/core/render-local-run.d.ts +82 -0
- package/dist/core/render-local-run.d.ts.map +1 -0
- package/dist/core/render-local-run.js +105 -0
- package/dist/core/render-local-run.js.map +1 -0
- package/dist/core/render-log-slice.d.ts +30 -0
- package/dist/core/render-log-slice.d.ts.map +1 -0
- package/dist/core/render-log-slice.js +49 -0
- package/dist/core/render-log-slice.js.map +1 -0
- package/dist/core/render-run-state.d.ts +9 -0
- package/dist/core/render-run-state.d.ts.map +1 -0
- package/dist/core/render-run-state.js +120 -0
- package/dist/core/render-run-state.js.map +1 -0
- package/dist/core/run-local-bootstrap.d.ts +93 -0
- package/dist/core/run-local-bootstrap.d.ts.map +1 -0
- package/dist/core/run-local-bootstrap.js +155 -0
- package/dist/core/run-local-bootstrap.js.map +1 -0
- package/dist/core/run-state.d.ts +60 -0
- package/dist/core/run-state.d.ts.map +1 -0
- package/dist/core/run-state.js +122 -0
- package/dist/core/run-state.js.map +1 -0
- package/dist/core/stub-feedback.d.ts +52 -0
- package/dist/core/stub-feedback.d.ts.map +1 -0
- package/dist/core/stub-feedback.js +33 -0
- package/dist/core/stub-feedback.js.map +1 -0
- package/dist/core/workflow-registry.d.ts +12 -1
- package/dist/core/workflow-registry.d.ts.map +1 -1
- package/dist/core/workflow-registry.js +2 -0
- package/dist/core/workflow-registry.js.map +1 -1
- package/dist/server/actions.d.ts +139 -0
- package/dist/server/actions.d.ts.map +1 -0
- package/dist/server/actions.js +430 -0
- package/dist/server/actions.js.map +1 -0
- package/dist/server/auth-routes.d.ts +92 -0
- package/dist/server/auth-routes.d.ts.map +1 -0
- package/dist/server/auth-routes.js +151 -0
- package/dist/server/auth-routes.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +0 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/rest.d.ts +5 -0
- package/dist/server/rest.d.ts.map +1 -1
- package/dist/server/rest.js +36 -12
- package/dist/server/rest.js.map +1 -1
- package/dist/server/runs.d.ts +34 -0
- package/dist/server/runs.d.ts.map +1 -0
- package/dist/server/runs.js +59 -0
- package/dist/server/runs.js.map +1 -0
- package/dist/server/static.d.ts +15 -1
- package/dist/server/static.d.ts.map +1 -1
- package/dist/server/static.js +54 -4
- package/dist/server/static.js.map +1 -1
- package/dist/shared/types.d.ts +136 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/web/assets/index-DCtyI4VW.css +32 -0
- package/dist/web/assets/index-WSw5RvYR.js +445 -0
- package/dist/web/canvas/sess-boot/index.html +305 -0
- package/dist/web/index.html +4 -9
- package/dist/web/welcome-hero-dark.png +0 -0
- package/dist/web/welcome-hero-light.png +0 -0
- package/package.json +6 -3
- package/dist/server/skills.d.ts +0 -51
- package/dist/server/skills.d.ts.map +0 -1
- package/dist/server/skills.js +0 -312
- package/dist/server/skills.js.map +0 -1
- package/dist/web/assets/index-BhKGSTTT.js +0 -214
- package/dist/web/assets/index-DRTrwTku.css +0 -32
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderRunState — the ONE pure mapper from a decoded {@link ExecutionProjection}
|
|
3
|
+
* to the canvas's {@link RunView} render state.
|
|
4
|
+
*
|
|
5
|
+
* Pure and deterministic: no LLM, no I/O, no clock. Every field is derived from
|
|
6
|
+
* the decoded projection, so the same function drives the polling path today and
|
|
7
|
+
* a future WebSocket push (only the data source swaps, never this mapping).
|
|
8
|
+
* Latency is read straight from the projection and rendered statically — the
|
|
9
|
+
* LLM is only ever invoked later by an explicit debug-macro press, never to
|
|
10
|
+
* compute what's shown here.
|
|
11
|
+
*
|
|
12
|
+
* Honest absence is preserved end to end: a still-running step gets no
|
|
13
|
+
* `latencyMs`, and a step with no logs gets no `logSlice`. The inspector
|
|
14
|
+
* surfaces logs, latency, and pass/fail only — no cost.
|
|
15
|
+
*/
|
|
16
|
+
import { isExecutionTerminal } from "@sapiom/agent-core";
|
|
17
|
+
// The log-buffer formatter lives in its own dependency-free module so the
|
|
18
|
+
// local-run mapper (imported by the browser SPA) can share the SAME formatter
|
|
19
|
+
// WITHOUT dragging this file's `@sapiom/agent-core` runtime import — and its
|
|
20
|
+
// `node:fs` reach — into the web bundle.
|
|
21
|
+
import { toLogSlice } from "./render-log-slice.js";
|
|
22
|
+
/**
|
|
23
|
+
* Fold the run's lifecycle status into the four states the UI distinguishes.
|
|
24
|
+
* Reuses `isExecutionTerminal` (agent-core's single source of truth for "won't
|
|
25
|
+
* advance on its own") so a newly-added terminal status can never be misdrawn as
|
|
26
|
+
* still-running: anything non-terminal (running, paused, queued, unknown) is
|
|
27
|
+
* `running`; the terminal set normalizes `canceled` → `cancelled`.
|
|
28
|
+
*/
|
|
29
|
+
function toRunStatus(raw) {
|
|
30
|
+
if (!isExecutionTerminal(raw))
|
|
31
|
+
return "running";
|
|
32
|
+
if (raw === "completed")
|
|
33
|
+
return "completed";
|
|
34
|
+
if (raw === "failed")
|
|
35
|
+
return "failed";
|
|
36
|
+
return "cancelled"; // the only remaining terminal statuses: "cancelled" / "canceled"
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fold a step's raw status into a {@link StepStatus}. Handles the real engine
|
|
40
|
+
* vocabulary (`succeeded`/`threw` from prod agents surface) plus the earlier
|
|
41
|
+
* values (`completed`/`failed`/`cancelled`/`canceled`). A cancelled or threw
|
|
42
|
+
* step folds to `failed` (it did not pass — mirrors run-local.ts); anything
|
|
43
|
+
* not yet running/passed/failed is `pending` (unstarted, queued, or unknown).
|
|
44
|
+
*/
|
|
45
|
+
function toStepStatus(raw) {
|
|
46
|
+
if (raw === "completed" || raw === "succeeded")
|
|
47
|
+
return "passed";
|
|
48
|
+
if (raw === "failed" ||
|
|
49
|
+
raw === "threw" ||
|
|
50
|
+
raw === "cancelled" ||
|
|
51
|
+
raw === "canceled")
|
|
52
|
+
return "failed";
|
|
53
|
+
if (raw === "running")
|
|
54
|
+
return "running";
|
|
55
|
+
return "pending";
|
|
56
|
+
}
|
|
57
|
+
/** `finishedAt − startedAt` in ms; `undefined` while still running (no finish)
|
|
58
|
+
* or when either timestamp is unparseable. A negative delta (clock skew) is
|
|
59
|
+
* dropped rather than shown as a misleading negative latency. */
|
|
60
|
+
function toLatencyMs(startedAt, finishedAt) {
|
|
61
|
+
if (!startedAt || !finishedAt)
|
|
62
|
+
return undefined;
|
|
63
|
+
const start = Date.parse(startedAt);
|
|
64
|
+
const end = Date.parse(finishedAt);
|
|
65
|
+
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
66
|
+
return undefined;
|
|
67
|
+
const delta = end - start;
|
|
68
|
+
return delta >= 0 ? delta : undefined;
|
|
69
|
+
}
|
|
70
|
+
/** Map one projection step to its render view. Optional fields are assigned only
|
|
71
|
+
* when present so absence stays absent (not `undefined`) in the JSON payload. */
|
|
72
|
+
function toStepView(step) {
|
|
73
|
+
const view = {
|
|
74
|
+
id: step.spanId ?? `step-${step.stepOrder}-${step.attempt}`,
|
|
75
|
+
name: step.stepName,
|
|
76
|
+
status: toStepStatus(step.status),
|
|
77
|
+
};
|
|
78
|
+
const latencyMs = toLatencyMs(step.startedAt, step.finishedAt);
|
|
79
|
+
if (latencyMs !== undefined)
|
|
80
|
+
view.latencyMs = latencyMs;
|
|
81
|
+
if (step.error?.message)
|
|
82
|
+
view.error = step.error.message;
|
|
83
|
+
const logSlice = toLogSlice(step.logs);
|
|
84
|
+
if (logSlice !== undefined)
|
|
85
|
+
view.logSlice = logSlice;
|
|
86
|
+
// Real per-step IO for the inspector's "Last run" block. The decoder
|
|
87
|
+
// collapses an absent input/output to `null` (its absence sentinel), so a
|
|
88
|
+
// `null` here means "the read carried nothing" — surfaced as ABSENT, never a
|
|
89
|
+
// fabricated payload. Any non-null value (including `0`, `false`, `""`) is a
|
|
90
|
+
// real payload and passes through. Capability, not model: these are the
|
|
91
|
+
// step's own values, with no provider/model surfaced.
|
|
92
|
+
if (step.input !== null)
|
|
93
|
+
view.input = step.input;
|
|
94
|
+
if (step.output !== null)
|
|
95
|
+
view.output = step.output;
|
|
96
|
+
// `step.events` are capability execution events forwarded by dispatched
|
|
97
|
+
// capabilities (e.g. tool_use / thinking / result events from a coding run).
|
|
98
|
+
// They do NOT represent dotted workflow capability calls (search.webSearch,
|
|
99
|
+
// memory.append, etc.) with args/results in the StepCall format, so we
|
|
100
|
+
// cannot map them to StepView.calls without fabricating structure that isn't
|
|
101
|
+
// there. `calls` is left absent for prod steps. The inspector's
|
|
102
|
+
// input/output/logs already carry the step-level evidence for prod runs;
|
|
103
|
+
// per-call detail is a fast-follow that requires a server-side addition to
|
|
104
|
+
// the projection shape (emitting dotted-capability call records alongside
|
|
105
|
+
// their args/results at the step level).
|
|
106
|
+
return view;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Map a decoded {@link ExecutionProjection} to the {@link RunView} the canvas
|
|
110
|
+
* renders. Order-preserving over `steps`; total (never throws) because the
|
|
111
|
+
* projection is already the decoded, degradation-tolerant shape.
|
|
112
|
+
*/
|
|
113
|
+
export function renderRunState(decoded) {
|
|
114
|
+
return {
|
|
115
|
+
executionId: decoded.id,
|
|
116
|
+
status: toRunStatus(decoded.status),
|
|
117
|
+
steps: decoded.steps.map(toStepView),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=render-run-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-run-state.js","sourceRoot":"","sources":["../../src/core/render-run-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAOzD,0EAA0E;AAC1E,8EAA8E;AAC9E,6EAA6E;AAC7E,yCAAyC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,GAAW;IAC9B,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAChD,IAAI,GAAG,KAAK,WAAW;QAAE,OAAO,WAAW,CAAC;IAC5C,IAAI,GAAG,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IACtC,OAAO,WAAW,CAAC,CAAC,iEAAiE;AACvF,CAAC;AAED;;;;;;GAMG;AACH,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,WAAW;QAAE,OAAO,QAAQ,CAAC;IAChE,IACE,GAAG,KAAK,QAAQ;QAChB,GAAG,KAAK,OAAO;QACf,GAAG,KAAK,WAAW;QACnB,GAAG,KAAK,UAAU;QAElB,OAAO,QAAQ,CAAC;IAClB,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;kEAEkE;AAClE,SAAS,WAAW,CAClB,SAAwB,EACxB,UAAyB;IAEzB,IAAI,CAAC,SAAS,IAAI,CAAC,UAAU;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACvE,MAAM,KAAK,GAAG,GAAG,GAAG,KAAK,CAAC;IAC1B,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACxC,CAAC;AAED;kFACkF;AAClF,SAAS,UAAU,CAAC,IAAoB;IACtC,MAAM,IAAI,GAAa;QACrB,EAAE,EAAE,IAAI,CAAC,MAAM,IAAI,QAAQ,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,OAAO,EAAE;QAC3D,IAAI,EAAE,IAAI,CAAC,QAAQ;QACnB,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC;KAClC,CAAC;IACF,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC/D,IAAI,SAAS,KAAK,SAAS;QAAE,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IACxD,IAAI,IAAI,CAAC,KAAK,EAAE,OAAO;QAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;IACzD,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,QAAQ,KAAK,SAAS;QAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACrD,qEAAqE;IACrE,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,wEAAwE;IACxE,sDAAsD;IACtD,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI;QAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACjD,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;QAAE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IACpD,wEAAwE;IACxE,6EAA6E;IAC7E,4EAA4E;IAC5E,uEAAuE;IACvE,6EAA6E;IAC7E,gEAAgE;IAChE,yEAAyE;IACzE,2EAA2E;IAC3E,0EAA0E;IAC1E,yCAAyC;IACzC,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,OAA4B;IACzD,OAAO;QACL,WAAW,EAAE,OAAO,CAAC,EAAE;QACvB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;QACnC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC;KACrC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-local bootstrap — the child-process entrypoint behind
|
|
3
|
+
* `POST /api/runs/local`.
|
|
4
|
+
*
|
|
5
|
+
* It runs an agent entirely in-process against stub capabilities
|
|
6
|
+
* (`runLocalFromDir` from @sapiom/agent-core) and writes the result as an
|
|
7
|
+
* NDJSON stream to stdout: one line per {@link LocalStepTrace}, then a single
|
|
8
|
+
* terminal summary line `{ outcome, output, error, unusedStubs, stubWarnings }`.
|
|
9
|
+
* Fully offline and zero-cost — run-local resolves every `ctx.sapiom.*` call
|
|
10
|
+
* from stubs and never touches the network.
|
|
11
|
+
*
|
|
12
|
+
* Why a separate child process (not an in-process import):
|
|
13
|
+
* 1. `runLocalFromDir` esbuild-bundles and dynamically `import()`s a workflow
|
|
14
|
+
* project the harness doesn't control, so a stray top-level side effect,
|
|
15
|
+
* infinite loop, or crash in someone else's step body is bounded to this
|
|
16
|
+
* one child instead of taking the long-lived harness server down.
|
|
17
|
+
* 2. It sidesteps a Vite/Vitest limitation: the dynamic `import(\`file://…\`)`
|
|
18
|
+
* inside the loader gets intercepted by the SSR dynamic-import-vars
|
|
19
|
+
* transform when pulled into a Vitest module graph, and mishandles the
|
|
20
|
+
* tmpdir `file://` URL on darwin. A plain child `node` process never goes
|
|
21
|
+
* through that transform. (Mirrors the reasoning in canvas-manifest-check.)
|
|
22
|
+
*
|
|
23
|
+
* Contract with the route ({@link createActionsRouter}):
|
|
24
|
+
* - Request arrives as one JSON object on **stdin**: `{ sourceDir, input?,
|
|
25
|
+
* stubs?, maxAttemptsPerStep? }`.
|
|
26
|
+
* - Output is line-oriented JSON on **stdout** — the route forwards each line
|
|
27
|
+
* through unchanged, so the shapes here ARE the wire shapes the SPA parses.
|
|
28
|
+
* - Diagnostics go to **stderr**; the route keeps a bounded tail for failures.
|
|
29
|
+
* - Exit 0 once the terminal line is written (even for a failed run — a failed
|
|
30
|
+
* *run* is a successful *invocation*); exit 1 only when no run happened
|
|
31
|
+
* (bad request, load error) after writing a terminal `error` line.
|
|
32
|
+
*/
|
|
33
|
+
import { type LocalRunOutcome, type StubFile } from "@sapiom/agent-core";
|
|
34
|
+
/** The request shape the route writes to this child's stdin. */
|
|
35
|
+
export interface RunLocalRequest {
|
|
36
|
+
/** Absolute path to the agent project directory (contains `index.ts`). */
|
|
37
|
+
sourceDir: string;
|
|
38
|
+
/** The workflow's entry-step input (optional; agent-core defaults it). */
|
|
39
|
+
input?: unknown;
|
|
40
|
+
/** Explicit stub overrides. When omitted the project's committed
|
|
41
|
+
* `.sapiom-dev/stubs.json` is used (agent-core's own precedence). */
|
|
42
|
+
stubs?: StubFile;
|
|
43
|
+
/** Per-step attempt cap (optional; agent-core defaults it). */
|
|
44
|
+
maxAttemptsPerStep?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The terminal NDJSON line, written after every per-step trace line. Carries
|
|
48
|
+
* the run-level outcome and the two stub-hygiene signals the inspector surfaces
|
|
49
|
+
* (WB15-2): `unusedStubs` (supplied keys that matched no capability call) and
|
|
50
|
+
* `stubWarnings` (keys that matched but carried the wrong shape).
|
|
51
|
+
*
|
|
52
|
+
* `kind: "summary"` discriminates it from a step-trace line so a consumer never
|
|
53
|
+
* has to guess which line is terminal.
|
|
54
|
+
*/
|
|
55
|
+
export interface RunLocalSummaryLine {
|
|
56
|
+
kind: "summary";
|
|
57
|
+
outcome: LocalRunOutcome;
|
|
58
|
+
output?: unknown;
|
|
59
|
+
error?: unknown;
|
|
60
|
+
unusedStubs: Array<{
|
|
61
|
+
step: string;
|
|
62
|
+
key: string;
|
|
63
|
+
}>;
|
|
64
|
+
stubWarnings: string[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The terminal line written when the run could not be *invoked* at all — a
|
|
68
|
+
* malformed request, an unreadable stub file, a project that fails to load.
|
|
69
|
+
* `outcome` is always `"failed"`; the child then exits non-zero. Distinct from
|
|
70
|
+
* a `summary` line, which represents a run that actually executed (even if that
|
|
71
|
+
* run's own outcome was `failed`).
|
|
72
|
+
*/
|
|
73
|
+
export interface RunLocalErrorLine {
|
|
74
|
+
kind: "error";
|
|
75
|
+
outcome: "failed";
|
|
76
|
+
error: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse the stdin payload into a {@link RunLocalRequest}. Throws a plain Error
|
|
80
|
+
* with a caller-safe message (no key material, no provider names) on anything
|
|
81
|
+
* that isn't a JSON object carrying a non-empty string `sourceDir`.
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseRunLocalRequest(raw: string): RunLocalRequest;
|
|
84
|
+
/**
|
|
85
|
+
* Run the request and emit its NDJSON stream to `out`. Returns the process exit
|
|
86
|
+
* code: 0 when a run executed (any outcome), 1 when the run could not be
|
|
87
|
+
* invoked (a terminal `error` line is emitted first). Never throws — every
|
|
88
|
+
* failure becomes an in-band terminal line, because a half-written stream with
|
|
89
|
+
* a thrown stack on stderr is far harder for the route to reason about than a
|
|
90
|
+
* clean terminal line plus an exit code.
|
|
91
|
+
*/
|
|
92
|
+
export declare function runBootstrap(request: RunLocalRequest, out: NodeJS.WritableStream): Promise<number>;
|
|
93
|
+
//# sourceMappingURL=run-local-bootstrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-local-bootstrap.d.ts","sourceRoot":"","sources":["../../src/core/run-local-bootstrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,QAAQ,EACd,MAAM,oBAAoB,CAAC;AAE5B,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;0EACsE;IACtE,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,+DAA+D;IAC/D,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,eAAe,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClD,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,QAAQ,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAWD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAwBjE;AAYD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE,MAAM,CAAC,cAAc,GACzB,OAAO,CAAC,MAAM,CAAC,CAkCjB"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-local bootstrap — the child-process entrypoint behind
|
|
3
|
+
* `POST /api/runs/local`.
|
|
4
|
+
*
|
|
5
|
+
* It runs an agent entirely in-process against stub capabilities
|
|
6
|
+
* (`runLocalFromDir` from @sapiom/agent-core) and writes the result as an
|
|
7
|
+
* NDJSON stream to stdout: one line per {@link LocalStepTrace}, then a single
|
|
8
|
+
* terminal summary line `{ outcome, output, error, unusedStubs, stubWarnings }`.
|
|
9
|
+
* Fully offline and zero-cost — run-local resolves every `ctx.sapiom.*` call
|
|
10
|
+
* from stubs and never touches the network.
|
|
11
|
+
*
|
|
12
|
+
* Why a separate child process (not an in-process import):
|
|
13
|
+
* 1. `runLocalFromDir` esbuild-bundles and dynamically `import()`s a workflow
|
|
14
|
+
* project the harness doesn't control, so a stray top-level side effect,
|
|
15
|
+
* infinite loop, or crash in someone else's step body is bounded to this
|
|
16
|
+
* one child instead of taking the long-lived harness server down.
|
|
17
|
+
* 2. It sidesteps a Vite/Vitest limitation: the dynamic `import(\`file://…\`)`
|
|
18
|
+
* inside the loader gets intercepted by the SSR dynamic-import-vars
|
|
19
|
+
* transform when pulled into a Vitest module graph, and mishandles the
|
|
20
|
+
* tmpdir `file://` URL on darwin. A plain child `node` process never goes
|
|
21
|
+
* through that transform. (Mirrors the reasoning in canvas-manifest-check.)
|
|
22
|
+
*
|
|
23
|
+
* Contract with the route ({@link createActionsRouter}):
|
|
24
|
+
* - Request arrives as one JSON object on **stdin**: `{ sourceDir, input?,
|
|
25
|
+
* stubs?, maxAttemptsPerStep? }`.
|
|
26
|
+
* - Output is line-oriented JSON on **stdout** — the route forwards each line
|
|
27
|
+
* through unchanged, so the shapes here ARE the wire shapes the SPA parses.
|
|
28
|
+
* - Diagnostics go to **stderr**; the route keeps a bounded tail for failures.
|
|
29
|
+
* - Exit 0 once the terminal line is written (even for a failed run — a failed
|
|
30
|
+
* *run* is a successful *invocation*); exit 1 only when no run happened
|
|
31
|
+
* (bad request, load error) after writing a terminal `error` line.
|
|
32
|
+
*/
|
|
33
|
+
import { runLocalFromDir, } from "@sapiom/agent-core";
|
|
34
|
+
/** Read the entire request payload from a readable stream as one UTF-8 string. */
|
|
35
|
+
async function readAll(stream) {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
for await (const chunk of stream) {
|
|
38
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
39
|
+
}
|
|
40
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse the stdin payload into a {@link RunLocalRequest}. Throws a plain Error
|
|
44
|
+
* with a caller-safe message (no key material, no provider names) on anything
|
|
45
|
+
* that isn't a JSON object carrying a non-empty string `sourceDir`.
|
|
46
|
+
*/
|
|
47
|
+
export function parseRunLocalRequest(raw) {
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error("run-local request is not valid JSON");
|
|
54
|
+
}
|
|
55
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
56
|
+
throw new Error("run-local request must be a JSON object");
|
|
57
|
+
}
|
|
58
|
+
const body = parsed;
|
|
59
|
+
const sourceDir = body.sourceDir;
|
|
60
|
+
if (typeof sourceDir !== "string" || sourceDir.trim() === "") {
|
|
61
|
+
throw new Error("run-local request requires a non-empty sourceDir");
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
sourceDir,
|
|
65
|
+
input: body.input,
|
|
66
|
+
stubs: body.stubs,
|
|
67
|
+
maxAttemptsPerStep: typeof body.maxAttemptsPerStep === "number"
|
|
68
|
+
? body.maxAttemptsPerStep
|
|
69
|
+
: undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Serialize one step trace as a single NDJSON line (newline-terminated). */
|
|
73
|
+
function traceLine(step) {
|
|
74
|
+
return JSON.stringify(step) + "\n";
|
|
75
|
+
}
|
|
76
|
+
/** Serialize the terminal summary as a single NDJSON line. */
|
|
77
|
+
function summaryLine(line) {
|
|
78
|
+
return JSON.stringify(line) + "\n";
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Run the request and emit its NDJSON stream to `out`. Returns the process exit
|
|
82
|
+
* code: 0 when a run executed (any outcome), 1 when the run could not be
|
|
83
|
+
* invoked (a terminal `error` line is emitted first). Never throws — every
|
|
84
|
+
* failure becomes an in-band terminal line, because a half-written stream with
|
|
85
|
+
* a thrown stack on stderr is far harder for the route to reason about than a
|
|
86
|
+
* clean terminal line plus an exit code.
|
|
87
|
+
*/
|
|
88
|
+
export async function runBootstrap(request, out) {
|
|
89
|
+
try {
|
|
90
|
+
const result = await runLocalFromDir({
|
|
91
|
+
sourceDir: request.sourceDir,
|
|
92
|
+
input: request.input,
|
|
93
|
+
stubs: request.stubs,
|
|
94
|
+
maxAttemptsPerStep: request.maxAttemptsPerStep,
|
|
95
|
+
});
|
|
96
|
+
// One line per step-attempt, in execution order — the consumer parses them
|
|
97
|
+
// incrementally rather than buffering the whole trace as a single blob.
|
|
98
|
+
for (const step of result.steps) {
|
|
99
|
+
out.write(traceLine(step));
|
|
100
|
+
}
|
|
101
|
+
const summary = {
|
|
102
|
+
kind: "summary",
|
|
103
|
+
outcome: result.outcome,
|
|
104
|
+
output: result.output,
|
|
105
|
+
error: result.error,
|
|
106
|
+
unusedStubs: result.unusedStubs,
|
|
107
|
+
stubWarnings: result.stubWarnings,
|
|
108
|
+
};
|
|
109
|
+
out.write(summaryLine(summary));
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
const errorLine = {
|
|
114
|
+
kind: "error",
|
|
115
|
+
outcome: "failed",
|
|
116
|
+
error: err instanceof Error ? err.message : String(err),
|
|
117
|
+
};
|
|
118
|
+
out.write(JSON.stringify(errorLine) + "\n");
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Entrypoint: read the request from stdin, run it, exit with the run's code.
|
|
124
|
+
* A stdin/read failure (or a request that can't be parsed) also degrades to a
|
|
125
|
+
* terminal `error` line + exit 1 — the route always sees a well-formed final
|
|
126
|
+
* line regardless of how the child failed.
|
|
127
|
+
*/
|
|
128
|
+
async function main() {
|
|
129
|
+
let request;
|
|
130
|
+
try {
|
|
131
|
+
request = parseRunLocalRequest(await readAll(process.stdin));
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
const errorLine = {
|
|
135
|
+
kind: "error",
|
|
136
|
+
outcome: "failed",
|
|
137
|
+
error: err instanceof Error ? err.message : String(err),
|
|
138
|
+
};
|
|
139
|
+
process.stdout.write(JSON.stringify(errorLine) + "\n");
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
process.exitCode = await runBootstrap(request, process.stdout);
|
|
144
|
+
}
|
|
145
|
+
// Only self-invoke when run as the child entrypoint, never when imported by a
|
|
146
|
+
// unit test (which drives runBootstrap / parseRunLocalRequest directly). The
|
|
147
|
+
// built entry is dist/core/run-local-bootstrap.js; import.meta.url ends with
|
|
148
|
+
// this module's own file, and process.argv[1] is the script node was told to
|
|
149
|
+
// run — they share a basename only for the real child launch.
|
|
150
|
+
const invokedAsScript = typeof process.argv[1] === "string" &&
|
|
151
|
+
import.meta.url === new URL(`file://${process.argv[1]}`).href;
|
|
152
|
+
if (invokedAsScript) {
|
|
153
|
+
void main();
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=run-local-bootstrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-local-bootstrap.js","sourceRoot":"","sources":["../../src/core/run-local-bootstrap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-state — fetches a prod execution projection from the agents surface and
|
|
3
|
+
* renders it to a {@link RunView} the canvas can poll.
|
|
4
|
+
*
|
|
5
|
+
* WHY the agents surface: deployed agent runs are owned by the agents runtime
|
|
6
|
+
* (`GET /agents/v1/executions/:id`), not the core-capabilities surface. The
|
|
7
|
+
* response is the full ExecutionProjection JSON (same shape agent-core's
|
|
8
|
+
* decodeExecutionProjection consumes).
|
|
9
|
+
*
|
|
10
|
+
* WHY the key stays server-side: the Sapiom API key is a harness credential that
|
|
11
|
+
* must never reach the browser. The harness server fetches on behalf of the
|
|
12
|
+
* canvas, so the SPA polls a local `/api/runs/:id/state` endpoint (no key in the
|
|
13
|
+
* request) rather than calling the upstream surface directly.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: this env-precedence helper for the agents base URL is duplicated
|
|
16
|
+
* elsewhere in the harness; consolidate into one shared helper when convenient.
|
|
17
|
+
*/
|
|
18
|
+
import type { RunView } from "../shared/types.js";
|
|
19
|
+
import { type ApiKeyProvider } from "./api-key-provider.js";
|
|
20
|
+
/** Resolve the agents surface base URL from the environment. */
|
|
21
|
+
export declare function resolveAgentsBaseUrl(): string;
|
|
22
|
+
export type RunStateResult = {
|
|
23
|
+
ok: true;
|
|
24
|
+
runView: RunView;
|
|
25
|
+
} | {
|
|
26
|
+
ok: false;
|
|
27
|
+
status: number;
|
|
28
|
+
error: string;
|
|
29
|
+
};
|
|
30
|
+
export interface RunStateFetcherOpts {
|
|
31
|
+
/**
|
|
32
|
+
* Sapiom API key. Accepts either a plain `string | null` (the boot-time key)
|
|
33
|
+
* or an {@link ApiKeyProvider} — pass a provider to get the refresh-on-401
|
|
34
|
+
* recovery path. A bare string never refreshes.
|
|
35
|
+
*/
|
|
36
|
+
apiKey: string | null | ApiKeyProvider;
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
/** Injectable fetch implementation — defaults to global fetch. Test seam. */
|
|
39
|
+
fetchImpl?: typeof fetch;
|
|
40
|
+
}
|
|
41
|
+
export interface RunStateFetcher {
|
|
42
|
+
fetch(executionId: string): Promise<RunStateResult>;
|
|
43
|
+
}
|
|
44
|
+
/** Upstream statuses that mean "the API key was rejected" — worth one refresh
|
|
45
|
+
* + retry before we give up. 401 is the common case; 403 covers a key that
|
|
46
|
+
* authenticated but lost authorization (e.g. rotated to a new org). */
|
|
47
|
+
export declare function isAuthRejection(status: number): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Create a fetcher that resolves an execution's current render state from the
|
|
50
|
+
* agents surface. The fetcher is intentionally non-throwing: all error paths
|
|
51
|
+
* return a typed {@link RunStateResult} with `ok: false` so the router can
|
|
52
|
+
* forward the appropriate HTTP status without a try/catch at the call site.
|
|
53
|
+
*
|
|
54
|
+
* When constructed with an {@link ApiKeyProvider}, a 401/403 from the upstream
|
|
55
|
+
* call triggers exactly one credential refresh + retry: if the shared store has
|
|
56
|
+
* a newer key (e.g. the user re-logged-in), the retry uses it and the Studio
|
|
57
|
+
* recovers in place instead of surfacing a dead-end auth error.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createRunStateFetcher(opts: RunStateFetcherOpts): RunStateFetcher;
|
|
60
|
+
//# sourceMappingURL=run-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../src/core/run-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EACL,KAAK,cAAc,EAEpB,MAAM,uBAAuB,CAAC;AAgB/B,gEAAgE;AAChE,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,cAAc,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACrD;AAED;;wEAEwE;AACxE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,mBAAmB,GACxB,eAAe,CA4EjB"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-state — fetches a prod execution projection from the agents surface and
|
|
3
|
+
* renders it to a {@link RunView} the canvas can poll.
|
|
4
|
+
*
|
|
5
|
+
* WHY the agents surface: deployed agent runs are owned by the agents runtime
|
|
6
|
+
* (`GET /agents/v1/executions/:id`), not the core-capabilities surface. The
|
|
7
|
+
* response is the full ExecutionProjection JSON (same shape agent-core's
|
|
8
|
+
* decodeExecutionProjection consumes).
|
|
9
|
+
*
|
|
10
|
+
* WHY the key stays server-side: the Sapiom API key is a harness credential that
|
|
11
|
+
* must never reach the browser. The harness server fetches on behalf of the
|
|
12
|
+
* canvas, so the SPA polls a local `/api/runs/:id/state` endpoint (no key in the
|
|
13
|
+
* request) rather than calling the upstream surface directly.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: this env-precedence helper for the agents base URL is duplicated
|
|
16
|
+
* elsewhere in the harness; consolidate into one shared helper when convenient.
|
|
17
|
+
*/
|
|
18
|
+
import { decodeExecutionProjection } from "@sapiom/agent-core";
|
|
19
|
+
import { renderRunState } from "./render-run-state.js";
|
|
20
|
+
import { staticApiKeyProvider, } from "./api-key-provider.js";
|
|
21
|
+
/**
|
|
22
|
+
* Normalize the polymorphic `apiKey` option to an {@link ApiKeyProvider}. A
|
|
23
|
+
* plain string (or null) becomes a no-refresh provider so existing static-key
|
|
24
|
+
* callers keep working unchanged; a provider is used as-is.
|
|
25
|
+
*/
|
|
26
|
+
function toApiKeyProvider(apiKey) {
|
|
27
|
+
if (apiKey === null || typeof apiKey === "string") {
|
|
28
|
+
return staticApiKeyProvider(apiKey);
|
|
29
|
+
}
|
|
30
|
+
return apiKey;
|
|
31
|
+
}
|
|
32
|
+
/** Resolve the agents surface base URL from the environment. */
|
|
33
|
+
export function resolveAgentsBaseUrl() {
|
|
34
|
+
return (process.env.SAPIOM_AGENTS_URL ??
|
|
35
|
+
process.env.SAPIOM_TOOLS_BASE ??
|
|
36
|
+
"https://tools.sapiom.ai");
|
|
37
|
+
}
|
|
38
|
+
/** Upstream statuses that mean "the API key was rejected" — worth one refresh
|
|
39
|
+
* + retry before we give up. 401 is the common case; 403 covers a key that
|
|
40
|
+
* authenticated but lost authorization (e.g. rotated to a new org). */
|
|
41
|
+
export function isAuthRejection(status) {
|
|
42
|
+
return status === 401 || status === 403;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a fetcher that resolves an execution's current render state from the
|
|
46
|
+
* agents surface. The fetcher is intentionally non-throwing: all error paths
|
|
47
|
+
* return a typed {@link RunStateResult} with `ok: false` so the router can
|
|
48
|
+
* forward the appropriate HTTP status without a try/catch at the call site.
|
|
49
|
+
*
|
|
50
|
+
* When constructed with an {@link ApiKeyProvider}, a 401/403 from the upstream
|
|
51
|
+
* call triggers exactly one credential refresh + retry: if the shared store has
|
|
52
|
+
* a newer key (e.g. the user re-logged-in), the retry uses it and the Studio
|
|
53
|
+
* recovers in place instead of surfacing a dead-end auth error.
|
|
54
|
+
*/
|
|
55
|
+
export function createRunStateFetcher(opts) {
|
|
56
|
+
const { baseUrl = resolveAgentsBaseUrl(), fetchImpl = fetch } = opts;
|
|
57
|
+
const provider = toApiKeyProvider(opts.apiKey);
|
|
58
|
+
const requestOnce = async (executionId, apiKey) => {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetchImpl(`${baseUrl}/agents/v1/executions/${encodeURIComponent(executionId)}`, { headers: { "x-sapiom-api-key": apiKey } });
|
|
61
|
+
return { kind: "ok", res };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return {
|
|
65
|
+
kind: "err",
|
|
66
|
+
result: { ok: false, status: 502, error: "gateway unreachable" },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const decode = async (res) => {
|
|
71
|
+
if (res.status === 404) {
|
|
72
|
+
return { ok: false, status: 404, error: "execution not found" };
|
|
73
|
+
}
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
status: 502,
|
|
78
|
+
error: `gateway responded ${res.status}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const raw = (await res.json());
|
|
83
|
+
const runView = renderRunState(decodeExecutionProjection(raw));
|
|
84
|
+
return { ok: true, runView };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { ok: false, status: 502, error: "could not decode execution" };
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
async fetch(executionId) {
|
|
92
|
+
// No API key — do not touch the network; the harness is not signed in.
|
|
93
|
+
let apiKey = provider.getKey();
|
|
94
|
+
if (!apiKey) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
status: 503,
|
|
98
|
+
error: "harness is not signed in to Sapiom",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const first = await requestOnce(executionId, apiKey);
|
|
102
|
+
if (first.kind === "err")
|
|
103
|
+
return first.result;
|
|
104
|
+
// Refresh-on-401: re-read the shared credential store once and retry with
|
|
105
|
+
// the newer key when the rejection was an auth failure and refresh
|
|
106
|
+
// actually produced a different key. Any other status (or an unchanged
|
|
107
|
+
// key) falls through to normal decoding/error mapping — no wasted retry.
|
|
108
|
+
if (isAuthRejection(first.res.status)) {
|
|
109
|
+
const refreshed = await provider.refresh();
|
|
110
|
+
if (refreshed && refreshed !== apiKey) {
|
|
111
|
+
apiKey = refreshed;
|
|
112
|
+
const second = await requestOnce(executionId, apiKey);
|
|
113
|
+
if (second.kind === "err")
|
|
114
|
+
return second.result;
|
|
115
|
+
return decode(second.res);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return decode(first.res);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=run-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-state.js","sourceRoot":"","sources":["../../src/core/run-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stub-feedback — the pure, read-only derivations behind the run-inspector's
|
|
3
|
+
* stub affordances (WB15-2). Given a {@link RunView} (which already carries the
|
|
4
|
+
* run-level stub facts set by {@link renderLocalRun}), decide:
|
|
5
|
+
* - whether a given step should show the "stubbed" chip, and
|
|
6
|
+
* - what the read-only stub-hygiene notice should say (or nothing).
|
|
7
|
+
*
|
|
8
|
+
* Pure and deterministic: no LLM, no I/O, no clock — every result is a function
|
|
9
|
+
* of the RunView alone, so both are Stryker targets with mutation-first tests.
|
|
10
|
+
*
|
|
11
|
+
* Read-only by design: this surfaces what a stub run DID (served a call, or a
|
|
12
|
+
* supplied stub matched nothing / had the wrong shape). Editing stubs is a
|
|
13
|
+
* deferred fast-follow and lives nowhere here.
|
|
14
|
+
*
|
|
15
|
+
* Honesty is the whole point: the chip appears only for a step that actually ran
|
|
16
|
+
* in a stub-served run (never for a prod run, never for a step the run never
|
|
17
|
+
* reached), and the notice is `null` unless there is a real problem to report —
|
|
18
|
+
* so a clean run shows no chrome at all.
|
|
19
|
+
*/
|
|
20
|
+
import type { RunView } from "../shared/types.js";
|
|
21
|
+
/**
|
|
22
|
+
* Should the "stubbed" chip render for the step named `stepName`?
|
|
23
|
+
*
|
|
24
|
+
* True only when BOTH hold:
|
|
25
|
+
* 1. the run was stub-served (`run.stubbed` — set by renderLocalRun for an
|
|
26
|
+
* offline local run; never by renderRunState for a prod run), and
|
|
27
|
+
* 2. that step actually ran this run (it appears in `run.steps`).
|
|
28
|
+
*
|
|
29
|
+
* A local run resolves every `ctx.sapiom.*` call from a stub, so a step that ran
|
|
30
|
+
* was stub-served — that is the honest granularity (agent-core records no
|
|
31
|
+
* per-CALL stub attribution). A step the run never reached, or any step of a
|
|
32
|
+
* real run, gets no chip. A null run (nothing observed yet) is never stubbed.
|
|
33
|
+
*/
|
|
34
|
+
export declare function stepIsStubbed(run: RunView | null, stepName: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The read-only stub-hygiene notice for a run, or `null` when there is nothing
|
|
37
|
+
* honest to show. Present only the signals that carry real content:
|
|
38
|
+
* - `unusedStubs`: supplied stub keys that matched no capability call (a no-op
|
|
39
|
+
* mock — almost always a typo or the wrong path form), and
|
|
40
|
+
* - `stubWarnings`: stub values that matched a key but had the wrong shape.
|
|
41
|
+
*
|
|
42
|
+
* Returns `null` unless at least one non-empty signal exists, so the caller can
|
|
43
|
+
* render nothing for a clean run (honesty — no empty "0 issues" panel). The
|
|
44
|
+
* returned arrays are always non-empty when present (empty ones are dropped),
|
|
45
|
+
* so the component never has to re-check length.
|
|
46
|
+
*/
|
|
47
|
+
export interface StubNotice {
|
|
48
|
+
unusedStubs?: NonNullable<RunView["unusedStubs"]>;
|
|
49
|
+
stubWarnings?: NonNullable<RunView["stubWarnings"]>;
|
|
50
|
+
}
|
|
51
|
+
export declare function stubNotice(run: RunView | null): StubNotice | null;
|
|
52
|
+
//# sourceMappingURL=stub-feedback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stub-feedback.d.ts","sourceRoot":"","sources":["../../src/core/stub-feedback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG5E;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;IAClD,YAAY,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC;CACrD;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,GAAG,UAAU,GAAG,IAAI,CAYjE"}
|