@inflexa-ai/harness 0.25.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/report-session-agent.d.ts +5 -4
- package/dist/agents/report-session-agent.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -3
- package/dist/lib/page-capture.d.ts +44 -15
- package/dist/lib/page-capture.js +78 -19
- package/dist/lib/zod-issues.d.ts +9 -4
- package/dist/lib/zod-issues.js +35 -4
- package/dist/loop/run-agent.js +36 -19
- package/dist/memory/card-builders.d.ts +4 -6
- package/dist/memory/card-builders.js +4 -6
- package/dist/memory/conversation-display-replay.d.ts +7 -6
- package/dist/memory/conversation-display-replay.js +7 -6
- package/dist/memory/conversation-display-storage.d.ts +2 -10
- package/dist/memory/conversation-display-storage.js +79 -32
- package/dist/memory/thread-history.d.ts +31 -20
- package/dist/memory/thread-history.js +45 -45
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +22 -13
- package/dist/runtime/assemble.d.ts +3 -0
- package/dist/runtime/assemble.js +9 -1
- package/dist/runtime/boot.d.ts +7 -8
- package/dist/runtime/boot.js +7 -19
- package/dist/sandbox/client.d.ts +9 -13
- package/dist/sandbox/client.js +8 -3
- package/dist/sandbox/create-sandbox.js +0 -1
- package/dist/tasks/derive-table-exec.d.ts +50 -0
- package/dist/tasks/derive-table-exec.js +84 -0
- package/dist/tools/define-tool.d.ts +19 -8
- package/dist/tools/define-tool.js +28 -12
- package/dist/tools/report-session/derive-table.d.ts +39 -7
- package/dist/tools/report-session/derive-table.js +24 -83
- package/dist/tools/report-session/examine-page.d.ts +22 -13
- package/dist/tools/report-session/examine-page.js +15 -10
- package/dist/tools/report-session/index.d.ts +1 -1
- package/dist/tools/research/generate-plan.js +3 -8
- package/dist/tools/research/inspect-data-profile.js +10 -11
- package/dist/tools/start-report-session.js +6 -2
- package/package.json +1 -1
- package/dist/app/data-profile-policy.d.ts +0 -62
- package/dist/app/data-profile-policy.js +0 -59
- package/dist/memory/content-to-cortex.d.ts +0 -75
- package/dist/memory/content-to-cortex.js +0 -201
- package/dist/memory/conversation-display-backfill.d.ts +0 -23
- package/dist/memory/conversation-display-backfill.js +0 -91
- package/dist/memory/reconstruct-cards.d.ts +0 -27
- package/dist/memory/reconstruct-cards.js +0 -57
package/dist/runtime/boot.js
CHANGED
|
@@ -20,27 +20,24 @@
|
|
|
20
20
|
* misconfigured pool fails loudly at boot, not under load.
|
|
21
21
|
* 5. `assembleCoreRuntime(core)` — registers the durable-workflow cohort and
|
|
22
22
|
* builds the conversation agent (register-before-launch invariant).
|
|
23
|
-
* 6. `
|
|
24
|
-
* display envelope and freezes one for each legacy turn that has none, so
|
|
25
|
-
* the runtime read path never meets a row without a display projection.
|
|
26
|
-
* Runs AFTER assembly because migrating a legacy call's one-line detail
|
|
27
|
-
* needs the assembled conversation roster, embedder-contributed tools
|
|
28
|
-
* included; assembly starts no traffic, so this is still before any read.
|
|
29
|
-
* 7. `beforeLaunch()` — embedder hook for host-specific pre-launch work that
|
|
23
|
+
* 6. `beforeLaunch()` — embedder hook for host-specific pre-launch work that
|
|
30
24
|
* must attach before DBOS launch re-emits events (scheduled sweeps, an
|
|
31
25
|
* legacy-workflow reap, an agent-switch install). Runs after registration
|
|
32
26
|
* so it may close over the registered callables.
|
|
33
|
-
*
|
|
27
|
+
* 7. `launchDbos(...)` — the last registration-dependent step.
|
|
28
|
+
*
|
|
29
|
+
* No step reads or rewrites stored conversation display. A boot that validated
|
|
30
|
+
* every `messages` row is what let a single retired part key fail startup for a
|
|
31
|
+
* whole deployment; the read tolerates such a row by itself now
|
|
32
|
+
* (`parseStoredDisplayEnvelope`), which is where a per-row concern belongs.
|
|
34
33
|
*
|
|
35
34
|
* Boot-step errors PROPAGATE (the embedder's composition root catches them and
|
|
36
35
|
* releases whatever it acquired). Only `shutdown` swallows per-step failures —
|
|
37
36
|
* once shutdown starts, exiting takes precedence (see `runShutdownSequence`).
|
|
38
37
|
*/
|
|
39
|
-
import { unwrapOrThrow } from "../lib/result.js";
|
|
40
38
|
import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
|
|
41
39
|
import { validateAgentSkills } from "../agents/sandbox/validate-skills.js";
|
|
42
40
|
import { initCortexState } from "../state/init.js";
|
|
43
|
-
import { backfillConversationDisplayEnvelopes } from "../memory/conversation-display-backfill.js";
|
|
44
41
|
import { assembleCoreRuntime } from "./assemble.js";
|
|
45
42
|
import { assertConnectionBudget } from "./connection-budget.js";
|
|
46
43
|
import { launchDbos, shutdownDbos } from "./dbos.js";
|
|
@@ -57,15 +54,6 @@ export async function bootHarness(deps) {
|
|
|
57
54
|
await initCortexState(pool);
|
|
58
55
|
await assertConnectionBudget({ pool, logger, config: deps.connectionBudget });
|
|
59
56
|
const runtime = assembleCoreRuntime(core);
|
|
60
|
-
// Assembly registers `conversation` unconditionally, so this resolution can
|
|
61
|
-
// never refuse; `unwrapOrThrow` turns the impossible error branch into a
|
|
62
|
-
// boot-propagating throw rather than threading a Result through the boot
|
|
63
|
-
// sequence, whose steps already fail by propagation.
|
|
64
|
-
const conversationAgent = unwrapOrThrow(runtime.agents.forThread("conversation"));
|
|
65
|
-
await backfillConversationDisplayEnvelopes({
|
|
66
|
-
pool,
|
|
67
|
-
tools: conversationAgent.tools,
|
|
68
|
-
});
|
|
69
57
|
await deps.beforeLaunch?.();
|
|
70
58
|
await launchDbos({ config: deps.dbos, logger });
|
|
71
59
|
logger.named("boot").info("harness booted", { executorId: deps.dbos.executorId });
|
package/dist/sandbox/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `SandboxClient` — the
|
|
2
|
+
* `SandboxClient` — the seam every sandbox-backed module in
|
|
3
3
|
* the harness sits on. Implementations are backend-selected
|
|
4
4
|
* (`docker`/`k8s`) by `createSandboxClient()` in `create-sandbox.ts` and
|
|
5
5
|
* injected at the composition root as a construction-time dependency
|
|
@@ -17,20 +17,16 @@
|
|
|
17
17
|
* Many execs may fire against the same machine.
|
|
18
18
|
* - The **exec** lifetime is one `submitExec → awaitExec`.
|
|
19
19
|
*
|
|
20
|
-
* `awaitExec` runs
|
|
21
|
-
*
|
|
20
|
+
* **Every caller of `awaitExec` runs inside a DBOS workflow body.** `DBOS.recv`
|
|
21
|
+
* and `DBOS.writeStream` are body-only, thus the callback transport cannot
|
|
22
|
+
* settle anywhere else, and the poll transport reaches for `DBOS.runStep` and
|
|
23
|
+
* `DBOS.sleepms` defaults. A caller that starts from a live chat turn registers
|
|
24
|
+
* its own workflow and starts it: `tasks/extract-values.ts` and
|
|
25
|
+
* `tasks/derive-table-exec.ts` are the two worked examples. No type carries this
|
|
26
|
+
* rule yet, thus a new caller must read this paragraph.
|
|
22
27
|
*/
|
|
23
|
-
import type { CreateSandboxMeta, ExecEmit, ExecResult, ManagedSandbox, SandboxIdentity, SandboxLiveness, SandboxRef,
|
|
28
|
+
import type { CreateSandboxMeta, ExecEmit, ExecResult, ManagedSandbox, SandboxIdentity, SandboxLiveness, SandboxRef, SubmitExecBody } from "./types.js";
|
|
24
29
|
export interface SandboxClient {
|
|
25
|
-
/**
|
|
26
|
-
* The result transport this client awaits under, fixed at construction. A caller that runs outside a
|
|
27
|
-
* workflow body reads it: `callback` awaits through `DBOS.recv`, which is body-only, thus such a caller
|
|
28
|
-
* must refuse before it starts a container it can never await.
|
|
29
|
-
*
|
|
30
|
-
* Optional, because a hand-written realization states nothing here. Absent means unknown, and a caller
|
|
31
|
-
* treats it as the poll default rather than refusing every composition that omits it.
|
|
32
|
-
*/
|
|
33
|
-
readonly transport?: SandboxTransport;
|
|
34
30
|
/**
|
|
35
31
|
* DBOS step (`sandbox.create`) — the spawn half of the two-step create
|
|
36
32
|
* (see the harness-sandbox-exec spec). Launches the sandbox-base container/Job under the
|
package/dist/sandbox/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `SandboxClient` — the
|
|
2
|
+
* `SandboxClient` — the seam every sandbox-backed module in
|
|
3
3
|
* the harness sits on. Implementations are backend-selected
|
|
4
4
|
* (`docker`/`k8s`) by `createSandboxClient()` in `create-sandbox.ts` and
|
|
5
5
|
* injected at the composition root as a construction-time dependency
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
* Many execs may fire against the same machine.
|
|
18
18
|
* - The **exec** lifetime is one `submitExec → awaitExec`.
|
|
19
19
|
*
|
|
20
|
-
* `awaitExec` runs
|
|
21
|
-
*
|
|
20
|
+
* **Every caller of `awaitExec` runs inside a DBOS workflow body.** `DBOS.recv`
|
|
21
|
+
* and `DBOS.writeStream` are body-only, thus the callback transport cannot
|
|
22
|
+
* settle anywhere else, and the poll transport reaches for `DBOS.runStep` and
|
|
23
|
+
* `DBOS.sleepms` defaults. A caller that starts from a live chat turn registers
|
|
24
|
+
* its own workflow and starts it: `tasks/extract-values.ts` and
|
|
25
|
+
* `tasks/derive-table-exec.ts` are the two worked examples. No type carries this
|
|
26
|
+
* rule yet, thus a new caller must read this paragraph.
|
|
22
27
|
*/
|
|
23
28
|
export {};
|
|
@@ -127,7 +127,6 @@ export function createSandboxClient(config) {
|
|
|
127
127
|
// poll loop's escalation probe are the same backend inspect.
|
|
128
128
|
const isAlive = async (ref) => unwrapOrThrow(await ops.isAlive(ref));
|
|
129
129
|
return {
|
|
130
|
-
transport,
|
|
131
130
|
createSandbox: async (meta, identity) => {
|
|
132
131
|
// The config states an engine fact (binds preserve host ownership);
|
|
133
132
|
// which remediation that fact demands — today, a world-writable step
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The container of one report-session derivation, as a registered DBOS workflow.
|
|
3
|
+
*
|
|
4
|
+
* The `derive_table` tool runs inside one live chat turn, and a chat turn is not a workflow body. An await
|
|
5
|
+
* of an exec under the callback transport calls `DBOS.recv`, which a workflow body alone can call. Thus the
|
|
6
|
+
* container lives here, behind a registered workflow, and the tool starts it and awaits the handle.
|
|
7
|
+
*
|
|
8
|
+
* The shape is the shape of `tasks/extract-values.ts`: a body that a test drives directly, a registration
|
|
9
|
+
* that closes over the construction-time deps, and a trigger that starts the workflow and gives the result
|
|
10
|
+
* back. The tool holds the trigger as a plain function, thus no module under `tools/` imports DBOS.
|
|
11
|
+
*
|
|
12
|
+
* The body owns the whole machine lifetime. It creates the container, submits one exec, awaits the terminal
|
|
13
|
+
* result, and tears the container down on both paths. A teardown fault reaches the log alone, because the
|
|
14
|
+
* work is done by then.
|
|
15
|
+
*/
|
|
16
|
+
import type { Logger } from "../lib/logger.js";
|
|
17
|
+
import type { SandboxClient } from "../sandbox/client.js";
|
|
18
|
+
import type { ExecResult } from "../sandbox/types.js";
|
|
19
|
+
import { type DeriveTableExecInput } from "../tools/report-session/derive-table.js";
|
|
20
|
+
/** The construction-time deps of the body. The registration closes over them, thus the trigger holds none. */
|
|
21
|
+
export interface DeriveTableExecDeps {
|
|
22
|
+
readonly sandboxClient: SandboxClient;
|
|
23
|
+
/**
|
|
24
|
+
* The checkpointed clock of the deadline. It defaults to `DBOS.now()`, which a replay reads again from
|
|
25
|
+
* the checkpoint. A test injects a fixed clock, thus it drives this body with no launched runtime.
|
|
26
|
+
*/
|
|
27
|
+
readonly now?: () => Promise<number>;
|
|
28
|
+
readonly logger?: Logger;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The body, exported so a test drives it without a registered workflow.
|
|
32
|
+
*
|
|
33
|
+
* A fault of any seam call throws. The tool reads a rejection as one short detail, thus the caller needs no
|
|
34
|
+
* result type here.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runDeriveTableExecBody(input: DeriveTableExecInput, deps: DeriveTableExecDeps): Promise<ExecResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Register the derivation workflow with DBOS. It gives back the registered callable, thus a trigger
|
|
39
|
+
* dispatches with `DBOS.startWorkflow`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function registerDeriveTableExecWorkflow(deps: DeriveTableExecDeps): (input: DeriveTableExecInput) => Promise<ExecResult>;
|
|
42
|
+
/** The per-attempt workflow id. The execution id is unique already, and the nonce keeps a retry distinct. */
|
|
43
|
+
export declare function deriveTableExecWorkflowId(executionId: string, nonce: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* The async edge. It starts the registered workflow and it awaits the terminal result.
|
|
46
|
+
*
|
|
47
|
+
* The tool authorizes the derivation before this call and it revokes after, thus this edge holds no
|
|
48
|
+
* lifecycle of its own. A workflow fault rejects the returned promise.
|
|
49
|
+
*/
|
|
50
|
+
export declare function triggerDeriveTableExec(workflow: (input: DeriveTableExecInput) => Promise<ExecResult>, input: DeriveTableExecInput): Promise<ExecResult>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The container of one report-session derivation, as a registered DBOS workflow.
|
|
3
|
+
*
|
|
4
|
+
* The `derive_table` tool runs inside one live chat turn, and a chat turn is not a workflow body. An await
|
|
5
|
+
* of an exec under the callback transport calls `DBOS.recv`, which a workflow body alone can call. Thus the
|
|
6
|
+
* container lives here, behind a registered workflow, and the tool starts it and awaits the handle.
|
|
7
|
+
*
|
|
8
|
+
* The shape is the shape of `tasks/extract-values.ts`: a body that a test drives directly, a registration
|
|
9
|
+
* that closes over the construction-time deps, and a trigger that starts the workflow and gives the result
|
|
10
|
+
* back. The tool holds the trigger as a plain function, thus no module under `tools/` imports DBOS.
|
|
11
|
+
*
|
|
12
|
+
* The body owns the whole machine lifetime. It creates the container, submits one exec, awaits the terminal
|
|
13
|
+
* result, and tears the container down on both paths. A teardown fault reaches the log alone, because the
|
|
14
|
+
* work is done by then.
|
|
15
|
+
*/
|
|
16
|
+
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
19
|
+
import { mintSandboxIdentity } from "../sandbox/identity.js";
|
|
20
|
+
import { buildDerivationExec, DERIVATION_DEADLINE_MS, DERIVATION_RESOURCES, DERIVE_RUN_LITERAL, DERIVE_STEP_LITERAL, } from "../tools/report-session/derive-table.js";
|
|
21
|
+
/** The awaitExec callback. A derivation reports no live activity, thus the callback drops each event. */
|
|
22
|
+
const noopEmit = () => { };
|
|
23
|
+
/**
|
|
24
|
+
* The body, exported so a test drives it without a registered workflow.
|
|
25
|
+
*
|
|
26
|
+
* A fault of any seam call throws. The tool reads a rejection as one short detail, thus the caller needs no
|
|
27
|
+
* result type here.
|
|
28
|
+
*/
|
|
29
|
+
export async function runDeriveTableExecBody(input, deps) {
|
|
30
|
+
const logger = (deps.logger ?? createNoopLogger()).named("derive-table-exec").with({ analysisId: input.analysisId });
|
|
31
|
+
const workflowId = DBOS.workflowID ?? `${DERIVE_RUN_LITERAL}:${input.executionId}`;
|
|
32
|
+
const sandbox = await deps.sandboxClient.createSandbox({
|
|
33
|
+
runId: DERIVE_RUN_LITERAL,
|
|
34
|
+
stepId: DERIVE_STEP_LITERAL,
|
|
35
|
+
analysisId: input.analysisId,
|
|
36
|
+
childWorkflowId: workflowId,
|
|
37
|
+
resources: DERIVATION_RESOURCES,
|
|
38
|
+
writableTail: input.writableTail,
|
|
39
|
+
}, mintSandboxIdentity(DERIVE_RUN_LITERAL));
|
|
40
|
+
try {
|
|
41
|
+
// A checkpointed clock, not `Date.now()`: the await gates on this absolute deadline, and a
|
|
42
|
+
// wall-clock deadline that grew on replay would shift which loop iteration crosses it.
|
|
43
|
+
const deadline = (await (deps.now ?? (() => DBOS.now()))()) + DERIVATION_DEADLINE_MS;
|
|
44
|
+
await deps.sandboxClient.submitExec(sandbox, buildDerivationExec({
|
|
45
|
+
script: input.script,
|
|
46
|
+
execId: input.executionId,
|
|
47
|
+
workingDir: input.workingDir,
|
|
48
|
+
inputs: input.inputs,
|
|
49
|
+
output: input.output,
|
|
50
|
+
}));
|
|
51
|
+
return await deps.sandboxClient.awaitExec(sandbox, input.executionId, noopEmit, deadline);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
try {
|
|
55
|
+
await deps.sandboxClient.teardown(sandbox);
|
|
56
|
+
}
|
|
57
|
+
catch (cause) {
|
|
58
|
+
logger.warn("the derivation sandbox did not tear down", logger.errorFields(cause));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Register the derivation workflow with DBOS. It gives back the registered callable, thus a trigger
|
|
64
|
+
* dispatches with `DBOS.startWorkflow`.
|
|
65
|
+
*/
|
|
66
|
+
export function registerDeriveTableExecWorkflow(deps) {
|
|
67
|
+
return DBOS.registerWorkflow((input) => runDeriveTableExecBody(input, deps), { name: "derive-table-exec" });
|
|
68
|
+
}
|
|
69
|
+
/** The per-attempt workflow id. The execution id is unique already, and the nonce keeps a retry distinct. */
|
|
70
|
+
export function deriveTableExecWorkflowId(executionId, nonce) {
|
|
71
|
+
return `${DERIVE_RUN_LITERAL}:${executionId}:${nonce}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The async edge. It starts the registered workflow and it awaits the terminal result.
|
|
75
|
+
*
|
|
76
|
+
* The tool authorizes the derivation before this call and it revokes after, thus this edge holds no
|
|
77
|
+
* lifecycle of its own. A workflow fault rejects the returned promise.
|
|
78
|
+
*/
|
|
79
|
+
export async function triggerDeriveTableExec(workflow, input) {
|
|
80
|
+
const handle = (await DBOS.startWorkflow(workflow, {
|
|
81
|
+
workflowID: deriveTableExecWorkflowId(input.executionId, randomUUID()),
|
|
82
|
+
})(input));
|
|
83
|
+
return handle.getResult();
|
|
84
|
+
}
|
|
@@ -39,9 +39,9 @@ export type ToolExecutionMode = "step" | "workflow" | "inline";
|
|
|
39
39
|
/** Runtime guard — does a `Result`'s error value carry the `ToolError` shape? */
|
|
40
40
|
export declare function isToolError(value: unknown): value is ToolError;
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
42
|
+
* One picture that a tool ok value carries beside its JSON data. `base64` holds
|
|
43
43
|
* the bytes, and `mediaType` names the IANA type, for example "image/png". The
|
|
44
|
-
* loop splits
|
|
44
|
+
* loop splits each picture out of the JSON text, and it rides the tool result as
|
|
45
45
|
* an image content block. Thus the model sees the picture, and the JSON text
|
|
46
46
|
* holds no bytes.
|
|
47
47
|
*/
|
|
@@ -50,23 +50,34 @@ export interface ToolResultImage {
|
|
|
50
50
|
readonly mediaType: string;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* The reserved key that carries
|
|
53
|
+
* The reserved key that carries the pictures on a tool ok value. It is a symbol,
|
|
54
54
|
* thus it never collides with a data field. `JSON.stringify` also omits a symbol
|
|
55
55
|
* key, thus the bytes never reach the JSON text by accident.
|
|
56
56
|
*/
|
|
57
57
|
export declare const toolResultImageKey: unique symbol;
|
|
58
|
-
/** A tool ok value that can carry
|
|
58
|
+
/** A tool ok value that can carry one picture or an ordered list of them under the reserved key. */
|
|
59
59
|
export type WithToolResultImage<T> = T & {
|
|
60
|
-
readonly [toolResultImageKey]?: ToolResultImage;
|
|
60
|
+
readonly [toolResultImageKey]?: ToolResultImage | readonly ToolResultImage[];
|
|
61
61
|
};
|
|
62
62
|
/**
|
|
63
|
-
* Attach
|
|
63
|
+
* Attach one picture to a tool ok value. The value keeps its own fields, and the
|
|
64
64
|
* picture rides under the reserved key. The loop reads the key, and it moves the
|
|
65
65
|
* bytes into an image content block on the tool result.
|
|
66
66
|
*/
|
|
67
67
|
export declare function withToolResultImage<T extends object>(value: T, image: ToolResultImage): WithToolResultImage<T>;
|
|
68
|
-
/**
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Attach an ordered list of pictures to a tool ok value. The order is the order
|
|
70
|
+
* that the model reads, and the loop keeps it on the wire. A tool that slices a
|
|
71
|
+
* page attaches the slices in document order.
|
|
72
|
+
*/
|
|
73
|
+
export declare function withToolResultImages<T extends object>(value: T, images: readonly ToolResultImage[]): WithToolResultImage<T>;
|
|
74
|
+
/**
|
|
75
|
+
* Read the pictures that a tool ok value carries, in order. A value with none
|
|
76
|
+
* gives the empty list. The one-picture shape reads as a list of one, thus a
|
|
77
|
+
* tool that attaches one picture and a tool that attaches a list meet the loop
|
|
78
|
+
* in the same shape.
|
|
79
|
+
*/
|
|
80
|
+
export declare function readToolResultImages(value: unknown): ToolResultImage[];
|
|
70
81
|
/**
|
|
71
82
|
* The request-scoped values passed to every tool's `execute`. No injected
|
|
72
83
|
* dependencies (see the harness-durable-runtime spec) — invocation identity,
|
|
@@ -21,30 +21,46 @@ export function isToolError(value) {
|
|
|
21
21
|
return typeof value === "object" && value !== null && typeof value.error === "string" && typeof value.retryable === "boolean";
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* The reserved key that carries
|
|
24
|
+
* The reserved key that carries the pictures on a tool ok value. It is a symbol,
|
|
25
25
|
* thus it never collides with a data field. `JSON.stringify` also omits a symbol
|
|
26
26
|
* key, thus the bytes never reach the JSON text by accident.
|
|
27
27
|
*/
|
|
28
28
|
export const toolResultImageKey = Symbol("toolResultImage");
|
|
29
29
|
/**
|
|
30
|
-
* Attach
|
|
30
|
+
* Attach one picture to a tool ok value. The value keeps its own fields, and the
|
|
31
31
|
* picture rides under the reserved key. The loop reads the key, and it moves the
|
|
32
32
|
* bytes into an image content block on the tool result.
|
|
33
33
|
*/
|
|
34
34
|
export function withToolResultImage(value, image) {
|
|
35
35
|
return { ...value, [toolResultImageKey]: image };
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Attach an ordered list of pictures to a tool ok value. The order is the order
|
|
39
|
+
* that the model reads, and the loop keeps it on the wire. A tool that slices a
|
|
40
|
+
* page attaches the slices in document order.
|
|
41
|
+
*/
|
|
42
|
+
export function withToolResultImages(value, images) {
|
|
43
|
+
return { ...value, [toolResultImageKey]: images };
|
|
44
|
+
}
|
|
45
|
+
/** Does one entry under the reserved key carry the picture shape? */
|
|
46
|
+
function isToolResultImage(entry) {
|
|
47
|
+
if (typeof entry !== "object" || entry === null)
|
|
48
|
+
return false;
|
|
49
|
+
const { base64, mediaType } = entry;
|
|
50
|
+
return typeof base64 === "string" && typeof mediaType === "string";
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read the pictures that a tool ok value carries, in order. A value with none
|
|
54
|
+
* gives the empty list. The one-picture shape reads as a list of one, thus a
|
|
55
|
+
* tool that attaches one picture and a tool that attaches a list meet the loop
|
|
56
|
+
* in the same shape.
|
|
57
|
+
*/
|
|
58
|
+
export function readToolResultImages(value) {
|
|
39
59
|
if (typeof value !== "object" || value === null)
|
|
40
|
-
return
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const { base64, mediaType } = image;
|
|
45
|
-
if (typeof base64 !== "string" || typeof mediaType !== "string")
|
|
46
|
-
return undefined;
|
|
47
|
-
return { base64, mediaType };
|
|
60
|
+
return [];
|
|
61
|
+
const carried = value[toolResultImageKey];
|
|
62
|
+
const entries = Array.isArray(carried) ? carried : [carried];
|
|
63
|
+
return entries.filter(isToolResultImage);
|
|
48
64
|
}
|
|
49
65
|
/**
|
|
50
66
|
* Package a `Tool`. The emitted JSON Schema must have a top-level
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* A session derivation is not an analysis run. It mints no run id, it registers no artifact, and it writes
|
|
9
9
|
* under the session directory alone. The rails are the rails of the value tier (`tasks/extract-values.ts`):
|
|
10
|
-
* the
|
|
10
|
+
* the injected exec runner, the identity mint, and the authorizer on every terminal path. The container is
|
|
11
11
|
* ephemeral, and it goes away with the work.
|
|
12
12
|
*
|
|
13
13
|
* The container mounts the analysis tree read-only, and one write mount covers the `derived/` directory of
|
|
@@ -29,15 +29,14 @@
|
|
|
29
29
|
* condition one time, up front, the same discipline as the eyes. A per-attempt failure would instead read as
|
|
30
30
|
* a transient fault, and it would invite a repeat of a call that can never pass.
|
|
31
31
|
*
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* tool refuses up front under it rather than start a container that nothing can await.
|
|
32
|
+
* The container runs behind a seam, and never in the turn. An await of an exec is a workflow-body call
|
|
33
|
+
* under the callback transport, and a report turn is not a body. Thus the tool takes an injected runner,
|
|
34
|
+
* and a registered workflow owns the container. The tool holds no sandbox client, and it imports no DBOS.
|
|
36
35
|
*/
|
|
37
36
|
import { z } from "zod";
|
|
38
37
|
import type { RunAuthorizer } from "../../execution/run-authorizer.js";
|
|
39
38
|
import { type Logger } from "../../lib/logger.js";
|
|
40
|
-
import type {
|
|
39
|
+
import type { ResourceSpec } from "../../config/resource-limits.js";
|
|
41
40
|
import type { ExecResult, SubmitExecBody } from "../../sandbox/types.js";
|
|
42
41
|
import type { DerivationSource, ReportSessionStateStore } from "../../state/report-session-state.js";
|
|
43
42
|
import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
|
|
@@ -113,10 +112,20 @@ export interface DeriveTableToolDeps {
|
|
|
113
112
|
readonly gateway: ReportSessionStateGateway;
|
|
114
113
|
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
115
114
|
readonly derivations: Pick<ReportSessionStateStore, "appendDerivation">;
|
|
116
|
-
readonly
|
|
115
|
+
readonly runDerivation?: DeriveTableRunner;
|
|
117
116
|
readonly runAuthorizer?: RunAuthorizer;
|
|
118
117
|
readonly logger?: Logger;
|
|
119
118
|
}
|
|
119
|
+
/** The synthetic run id and step id of one derivation. Both are constants, and neither names a run row. */
|
|
120
|
+
export declare const DERIVE_RUN_LITERAL: "derive-table";
|
|
121
|
+
export declare const DERIVE_STEP_LITERAL: "derive";
|
|
122
|
+
/** The exec budget of one derivation. It matches the budget of the value tier. */
|
|
123
|
+
export declare const DERIVATION_DEADLINE_MS = 300000;
|
|
124
|
+
/**
|
|
125
|
+
* The container size of one derivation. A join or a pivot loads each declared input into pandas at one
|
|
126
|
+
* time, thus the container needs the headroom of the value tier.
|
|
127
|
+
*/
|
|
128
|
+
export declare const DERIVATION_RESOURCES: ResourceSpec;
|
|
120
129
|
/** The environment variable that carries the declared inputs into the container. */
|
|
121
130
|
export declare const DERIVE_INPUT_ENV = "DERIVE_INPUTS";
|
|
122
131
|
/** The environment variable that carries the container path of the output file into the container. */
|
|
@@ -152,6 +161,29 @@ export declare function buildDerivationExec(args: {
|
|
|
152
161
|
* The function is pure, thus a test asserts the mapping without a sandbox.
|
|
153
162
|
*/
|
|
154
163
|
export declare function describeExecFailure(result: ExecResult): string | undefined;
|
|
164
|
+
/**
|
|
165
|
+
* The exec of one derivation, as a registered workflow reads it.
|
|
166
|
+
*
|
|
167
|
+
* Each field is plain data, thus the value crosses the workflow boundary and it survives a replay. The
|
|
168
|
+
* paths are container paths already: the host maps them one time, before the start.
|
|
169
|
+
*/
|
|
170
|
+
export interface DeriveTableExecInput {
|
|
171
|
+
readonly analysisId: string;
|
|
172
|
+
readonly executionId: string;
|
|
173
|
+
readonly script: string;
|
|
174
|
+
readonly writableTail: string;
|
|
175
|
+
readonly workingDir: string;
|
|
176
|
+
readonly inputs: readonly DerivationInputMount[];
|
|
177
|
+
readonly output: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Run one derivation exec and give the terminal result back.
|
|
181
|
+
*
|
|
182
|
+
* The composition realizes it over a registered workflow, thus the container lives inside a workflow body
|
|
183
|
+
* and the await is legal under each transport. A fault of the sandbox rejects the promise, and the tool
|
|
184
|
+
* turns that rejection into one short detail.
|
|
185
|
+
*/
|
|
186
|
+
export type DeriveTableRunner = (input: DeriveTableExecInput) => Promise<ExecResult>;
|
|
155
187
|
/**
|
|
156
188
|
* Make the derivation tool over the session-state gateway, the derivation ledger, and the sandbox rails.
|
|
157
189
|
*
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* A session derivation is not an analysis run. It mints no run id, it registers no artifact, and it writes
|
|
9
9
|
* under the session directory alone. The rails are the rails of the value tier (`tasks/extract-values.ts`):
|
|
10
|
-
* the
|
|
10
|
+
* the injected exec runner, the identity mint, and the authorizer on every terminal path. The container is
|
|
11
11
|
* ephemeral, and it goes away with the work.
|
|
12
12
|
*
|
|
13
13
|
* The container mounts the analysis tree read-only, and one write mount covers the `derived/` directory of
|
|
@@ -29,12 +29,11 @@
|
|
|
29
29
|
* condition one time, up front, the same discipline as the eyes. A per-attempt failure would instead read as
|
|
30
30
|
* a transient fault, and it would invite a repeat of a call that can never pass.
|
|
31
31
|
*
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* tool refuses up front under it rather than start a container that nothing can await.
|
|
32
|
+
* The container runs behind a seam, and never in the turn. An await of an exec is a workflow-body call
|
|
33
|
+
* under the callback transport, and a report turn is not a body. Thus the tool takes an injected runner,
|
|
34
|
+
* and a registered workflow owns the container. The tool holds no sandbox client, and it imports no DBOS.
|
|
36
35
|
*/
|
|
37
|
-
import {
|
|
36
|
+
import { ok } from "neverthrow";
|
|
38
37
|
import { unlink } from "node:fs/promises";
|
|
39
38
|
import { join } from "node:path";
|
|
40
39
|
import { z } from "zod";
|
|
@@ -43,7 +42,6 @@ import { classifyWithinRoot, computeSha256, computeSha256File } from "../../lib/
|
|
|
43
42
|
import { defaultErrorFields } from "../../lib/logger.js";
|
|
44
43
|
import { snapshotEntry } from "../../report-model/reference-resolver.js";
|
|
45
44
|
import { generateExecutionId } from "../../sandbox/execution-id.js";
|
|
46
|
-
import { mintSandboxIdentity } from "../../sandbox/identity.js";
|
|
47
45
|
import { isSafeId, reportSessionDerivedDir, toSandboxPath } from "../../workspace/paths.js";
|
|
48
46
|
import { defineTool } from "../define-tool.js";
|
|
49
47
|
import { openReportThread } from "../report-authoring/authoring-tools.js";
|
|
@@ -55,17 +53,17 @@ const deriveTableInput = z.object({
|
|
|
55
53
|
output: z.string().min(1),
|
|
56
54
|
});
|
|
57
55
|
/** The synthetic run id and step id of one derivation. Both are constants, and neither names a run row. */
|
|
58
|
-
const DERIVE_RUN_LITERAL = "derive-table";
|
|
59
|
-
const DERIVE_STEP_LITERAL = "derive";
|
|
56
|
+
export const DERIVE_RUN_LITERAL = "derive-table";
|
|
57
|
+
export const DERIVE_STEP_LITERAL = "derive";
|
|
60
58
|
/** The provenance agent id. No agent loop runs in the container, thus this names the derivation pass. */
|
|
61
59
|
const DERIVE_AGENT_ID = "table-deriver";
|
|
62
60
|
/** The exec budget of one derivation. It matches the budget of the value tier. */
|
|
63
|
-
const DERIVATION_DEADLINE_MS = 300_000;
|
|
61
|
+
export const DERIVATION_DEADLINE_MS = 300_000;
|
|
64
62
|
/**
|
|
65
63
|
* The container size of one derivation. A join or a pivot loads each declared input into pandas at one
|
|
66
64
|
* time, thus the container needs the headroom of the value tier.
|
|
67
65
|
*/
|
|
68
|
-
const DERIVATION_RESOURCES = { cpu: 2, memoryGb: 8 };
|
|
66
|
+
export const DERIVATION_RESOURCES = { cpu: 2, memoryGb: 8 };
|
|
69
67
|
/** The cap of the script, in bytes. A reshaping script sits far under it, and the command line carries it. */
|
|
70
68
|
const SCRIPT_CAP_BYTES = 64 * 1024;
|
|
71
69
|
/** The cap of the declared inputs. One derivation reads the evidence of a few artifacts, and never a tree. */
|
|
@@ -76,12 +74,8 @@ export const DERIVE_INPUT_ENV = "DERIVE_INPUTS";
|
|
|
76
74
|
export const DERIVE_OUTPUT_ENV = "DERIVE_OUTPUT";
|
|
77
75
|
/** The cap of the failure detail that the tool copies out of the standard error of the script. */
|
|
78
76
|
const STDERR_TAIL_CAP = 2_000;
|
|
79
|
-
/** The awaitExec callback. A derivation reports no live activity, thus the callback drops each event. */
|
|
80
|
-
const noopEmit = () => { };
|
|
81
77
|
/** The line that the agent reads when the composition binds no sandbox rails. */
|
|
82
78
|
const NO_SANDBOX_DETAIL = "the composition gives no sandbox, thus this session cannot derive a table";
|
|
83
|
-
/** The line that the agent reads when the sandbox client awaits under a transport that a turn cannot drive. */
|
|
84
|
-
const CALLBACK_TRANSPORT_DETAIL = "the sandbox of this composition reports through callbacks, which a report turn cannot await, thus it derives no table";
|
|
85
79
|
/**
|
|
86
80
|
* Build the sandbox exec of one derivation. The command runs the script of the agent through `python3 -c`.
|
|
87
81
|
*
|
|
@@ -126,58 +120,6 @@ export function describeExecFailure(result) {
|
|
|
126
120
|
}
|
|
127
121
|
return undefined;
|
|
128
122
|
}
|
|
129
|
-
/**
|
|
130
|
-
* Run one derivation in an ephemeral container, and give the exec result back.
|
|
131
|
-
*
|
|
132
|
-
* The container mounts the analysis tree read-only, and the declared write tail is its one writable mount.
|
|
133
|
-
* Thus the script reads the evidence and it writes into the `derived/` directory of the session and nowhere
|
|
134
|
-
* else. The teardown runs on both paths, thus no machine outlives the call. A teardown fault reaches the log
|
|
135
|
-
* alone, because the work is done.
|
|
136
|
-
*
|
|
137
|
-
* Each seam call is guarded, thus a fault of the sandbox becomes a short detail and the tool keeps its
|
|
138
|
-
* no-throw contract.
|
|
139
|
-
*/
|
|
140
|
-
async function runDerivationExec(args) {
|
|
141
|
-
let sandbox;
|
|
142
|
-
try {
|
|
143
|
-
sandbox = await args.client.createSandbox({
|
|
144
|
-
runId: DERIVE_RUN_LITERAL,
|
|
145
|
-
stepId: DERIVE_STEP_LITERAL,
|
|
146
|
-
analysisId: args.analysisId,
|
|
147
|
-
childWorkflowId: `${DERIVE_RUN_LITERAL}:${args.executionId}`,
|
|
148
|
-
resources: DERIVATION_RESOURCES,
|
|
149
|
-
writableTail: args.writableTail,
|
|
150
|
-
}, mintSandboxIdentity(DERIVE_RUN_LITERAL));
|
|
151
|
-
}
|
|
152
|
-
catch (cause) {
|
|
153
|
-
args.logger.error("the derivation sandbox did not start", args.logger.errorFields(cause));
|
|
154
|
-
return err("the derivation sandbox did not start");
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
// The tool runs inside one live turn and it never replays, thus the wall clock is the deadline.
|
|
158
|
-
const deadline = Date.now() + DERIVATION_DEADLINE_MS;
|
|
159
|
-
await args.client.submitExec(sandbox, buildDerivationExec({
|
|
160
|
-
script: args.script,
|
|
161
|
-
execId: args.executionId,
|
|
162
|
-
workingDir: args.workingDir,
|
|
163
|
-
inputs: args.inputs,
|
|
164
|
-
output: args.output,
|
|
165
|
-
}));
|
|
166
|
-
return ok(await args.client.awaitExec(sandbox, args.executionId, noopEmit, deadline));
|
|
167
|
-
}
|
|
168
|
-
catch (cause) {
|
|
169
|
-
args.logger.error("the derivation exec did not complete", args.logger.errorFields(cause));
|
|
170
|
-
return err("the derivation exec did not complete");
|
|
171
|
-
}
|
|
172
|
-
finally {
|
|
173
|
-
try {
|
|
174
|
-
await args.client.teardown(sandbox);
|
|
175
|
-
}
|
|
176
|
-
catch (cause) {
|
|
177
|
-
args.logger.warn("the derivation sandbox did not tear down", args.logger.errorFields(cause));
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
123
|
/**
|
|
182
124
|
* Make the derivation tool over the session-state gateway, the derivation ledger, and the sandbox rails.
|
|
183
125
|
*
|
|
@@ -186,7 +128,7 @@ async function runDerivationExec(args) {
|
|
|
186
128
|
*/
|
|
187
129
|
export function createDeriveTableTool(deps) {
|
|
188
130
|
const logger = (deps.logger ?? createNoopLogger()).named("derive-table");
|
|
189
|
-
const {
|
|
131
|
+
const { runDerivation, runAuthorizer } = deps;
|
|
190
132
|
return defineTool({
|
|
191
133
|
id: "derive_table",
|
|
192
134
|
description: "Derive one table from the pinned evidence with a Python script, and pin the result to this session. " +
|
|
@@ -209,16 +151,10 @@ export function createDeriveTableTool(deps) {
|
|
|
209
151
|
execute: async (input, ctx) => {
|
|
210
152
|
// The check runs before every read, thus one clear signal replaces a failure for each attempt.
|
|
211
153
|
// It also narrows the two rails below, thus the exec needs no assertion.
|
|
212
|
-
if (
|
|
154
|
+
if (runDerivation === undefined || runAuthorizer === undefined) {
|
|
213
155
|
logger.warn("the composition gives no sandbox, thus no derivation can run");
|
|
214
156
|
return ok({ outcome: "unavailable", detail: NO_SANDBOX_DETAIL });
|
|
215
157
|
}
|
|
216
|
-
// The await of the callback transport is a workflow-body call, and a report turn is not one. An
|
|
217
|
-
// absent field states nothing, thus it reads as the poll default and it refuses nothing.
|
|
218
|
-
if (sandboxClient.transport === "callback") {
|
|
219
|
-
logger.warn("the sandbox client awaits through callbacks, thus no derivation can run in a turn");
|
|
220
|
-
return ok({ outcome: "unavailable", detail: CALLBACK_TRANSPORT_DETAIL });
|
|
221
|
-
}
|
|
222
158
|
const opened = await openReportThread(deps.gateway, ctx.session.scope);
|
|
223
159
|
if (opened.isErr()) {
|
|
224
160
|
return ok({ outcome: "refused", refusal: opened.error });
|
|
@@ -304,7 +240,7 @@ export function createDeriveTableTool(deps) {
|
|
|
304
240
|
let result;
|
|
305
241
|
try {
|
|
306
242
|
result = await derive({
|
|
307
|
-
|
|
243
|
+
runDerivation,
|
|
308
244
|
derivations: deps.derivations,
|
|
309
245
|
root,
|
|
310
246
|
threadId,
|
|
@@ -363,8 +299,9 @@ async function derive(args) {
|
|
|
363
299
|
return { outcome: "exec-failed", detail: `${args.outputName} already holds a file that did not clear, thus this derivation takes a new name` };
|
|
364
300
|
}
|
|
365
301
|
}
|
|
366
|
-
|
|
367
|
-
|
|
302
|
+
// The path mapper refuses a stored path that escapes the root, with a throw. It runs before the try,
|
|
303
|
+
// thus that refusal stays a throw and it never reads as a fault of the container.
|
|
304
|
+
const execInput = {
|
|
368
305
|
analysisId: args.analysisId,
|
|
369
306
|
executionId,
|
|
370
307
|
script: args.script,
|
|
@@ -372,12 +309,16 @@ async function derive(args) {
|
|
|
372
309
|
workingDir,
|
|
373
310
|
inputs: args.sources.map((source) => ({ path: toSandboxPath(args.root, args.analysisId, join(args.root, source.path)), hash: source.hash })),
|
|
374
311
|
output: toSandboxPath(args.root, args.analysisId, absolute),
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
312
|
+
};
|
|
313
|
+
let executed;
|
|
314
|
+
try {
|
|
315
|
+
executed = await args.runDerivation(execInput);
|
|
316
|
+
}
|
|
317
|
+
catch (cause) {
|
|
318
|
+
args.logger.error("the derivation exec did not complete", { threadId: args.threadId, ...defaultErrorFields(cause) });
|
|
319
|
+
return { outcome: "exec-failed", detail: "the derivation exec did not complete" };
|
|
379
320
|
}
|
|
380
|
-
const failure = describeExecFailure(executed
|
|
321
|
+
const failure = describeExecFailure(executed);
|
|
381
322
|
if (failure !== undefined) {
|
|
382
323
|
return { outcome: "exec-failed", detail: failure };
|
|
383
324
|
}
|