@inflexa-ai/harness 0.19.2 → 0.21.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/dist/agents/conversation-agent.d.ts +26 -14
- package/dist/agents/conversation-agent.js +12 -23
- package/dist/agents/report-session-agent.d.ts +31 -5
- package/dist/agents/report-session-agent.js +31 -6
- package/dist/app/chat-turn.js +3 -1
- package/dist/app/message-assembly.d.ts +15 -1
- package/dist/app/message-assembly.js +22 -8
- package/dist/app/report-session-runtime.d.ts +21 -4
- package/dist/app/report-session-runtime.js +53 -6
- package/dist/app/spawn-report-session.d.ts +174 -17
- package/dist/app/spawn-report-session.js +233 -21
- package/dist/contracts/chat-events.d.ts +7 -2
- package/dist/contracts/message.d.ts +10 -0
- package/dist/contracts/report-blocks.d.ts +743 -19
- package/dist/contracts/report-blocks.js +186 -11
- package/dist/contracts/report-reference.d.ts +88 -0
- package/dist/contracts/report-reference.js +48 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -0
- package/dist/lib/chrome.d.ts +26 -6
- package/dist/lib/chrome.js +99 -32
- package/dist/lib/eyes.d.ts +57 -0
- package/dist/lib/eyes.js +45 -0
- package/dist/lib/page-capture.d.ts +14 -0
- package/dist/lib/page-capture.js +56 -2
- package/dist/literature/sources/pubmed.js +2 -1
- package/dist/loop/run-agent.js +58 -23
- package/dist/loop/tool-detail.d.ts +19 -2
- package/dist/loop/tool-detail.js +35 -2
- package/dist/memory/content-to-cortex.d.ts +3 -2
- package/dist/memory/content-to-cortex.js +21 -12
- package/dist/memory/conversation-display-replay.js +14 -7
- package/dist/memory/thread-history.d.ts +60 -3
- package/dist/memory/thread-history.js +55 -16
- package/dist/memory/thread-store.d.ts +11 -1
- package/dist/memory/thread-store.js +12 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +23 -6
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +183 -9
- package/dist/report-model/authoring-grammar.d.ts +463 -0
- package/dist/report-model/authoring-grammar.js +77 -0
- package/dist/report-model/block-walk.d.ts +57 -14
- package/dist/report-model/block-walk.js +116 -11
- package/dist/report-model/draft-finish.d.ts +22 -4
- package/dist/report-model/draft-finish.js +39 -7
- package/dist/report-model/draft-operations.d.ts +3 -0
- package/dist/report-model/draft-operations.js +19 -5
- package/dist/report-model/draft-read.d.ts +8 -5
- package/dist/report-model/draft-read.js +21 -5
- package/dist/report-model/fixture-resolver.js +15 -4
- package/dist/report-model/pin-snapshot.d.ts +24 -6
- package/dist/report-model/pin-snapshot.js +204 -6
- package/dist/report-model/production-resolver.js +15 -4
- package/dist/report-model/reference-resolver.d.ts +50 -1
- package/dist/report-model/reference-resolver.js +94 -1
- package/dist/report-model/reference-stamp.d.ts +45 -0
- package/dist/report-model/reference-stamp.js +173 -0
- package/dist/report-model/structural-validation.d.ts +4 -1
- package/dist/report-model/structural-validation.js +48 -3
- package/dist/report-model/validate.d.ts +1 -1
- package/dist/report-model/validate.js +1 -1
- package/dist/report-render/asset-lookup.d.ts +19 -0
- package/dist/report-render/asset-lookup.js +37 -0
- package/dist/report-render/assets.d.ts +45 -1
- package/dist/report-render/assets.js +65 -6
- package/dist/report-render/chart-presets.d.ts +102 -0
- package/dist/report-render/chart-presets.js +205 -0
- package/dist/report-render/chart.d.ts +112 -10
- package/dist/report-render/chart.js +1164 -41
- package/dist/report-render/design.d.ts +99 -0
- package/dist/report-render/design.js +289 -107
- package/dist/report-render/number-format.d.ts +117 -0
- package/dist/report-render/number-format.js +479 -0
- package/dist/report-render/page.d.ts +121 -5
- package/dist/report-render/page.js +689 -7
- package/dist/report-render/references.d.ts +70 -16
- package/dist/report-render/references.js +54 -22
- package/dist/report-render/render.d.ts +23 -5
- package/dist/report-render/render.js +81 -22
- package/dist/report-render/table-data.d.ts +92 -0
- package/dist/report-render/table-data.js +129 -0
- package/dist/report-render/types.d.ts +15 -0
- package/dist/report-render/value-bridge.js +6 -2
- package/dist/report-render/views/chart-view.d.ts +12 -5
- package/dist/report-render/views/chart-view.js +14 -8
- package/dist/report-render/views/page-view.d.ts +36 -15
- package/dist/report-render/views/page-view.js +45 -23
- package/dist/report-render/views/prose.d.ts +12 -2
- package/dist/report-render/views/prose.js +29 -7
- package/dist/report-render/views/references-view.d.ts +22 -8
- package/dist/report-render/views/references-view.js +101 -13
- package/dist/report-render/views/values.d.ts +128 -13
- package/dist/report-render/views/values.js +199 -18
- package/dist/runtime/assemble.d.ts +39 -5
- package/dist/runtime/assemble.js +47 -6
- package/dist/sandbox/client.d.ts +10 -1
- package/dist/sandbox/create-sandbox.d.ts +9 -4
- package/dist/sandbox/create-sandbox.js +22 -16
- package/dist/sandbox/docker-client.js +10 -7
- package/dist/sandbox/k8s-client.js +11 -18
- package/dist/sandbox/mount-plan.d.ts +45 -7
- package/dist/sandbox/mount-plan.js +43 -5
- package/dist/sandbox/types.d.ts +10 -6
- package/dist/state/init.js +38 -9
- package/dist/state/report-session-state.d.ts +42 -1
- package/dist/state/report-session-state.js +70 -2
- package/dist/state/report-versions.d.ts +22 -17
- package/dist/state/report-versions.js +48 -35
- package/dist/state/snapshot-parse.js +16 -1
- package/dist/tasks/data-profile.d.ts +7 -4
- package/dist/tasks/data-profile.js +9 -9
- package/dist/tools/define-tool.d.ts +32 -0
- package/dist/tools/define-tool.js +4 -0
- package/dist/tools/detail-resolver.d.ts +7 -2
- package/dist/tools/detail-resolver.js +7 -2
- package/dist/tools/iterate-report.d.ts +1 -1
- package/dist/tools/report/preview-snapshot.js +1 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +15 -6
- package/dist/tools/report-authoring/authoring-tools.js +99 -18
- package/dist/tools/report-session/derive-table.d.ts +162 -0
- package/dist/tools/report-session/derive-table.js +459 -0
- package/dist/tools/report-session/examine-page.d.ts +48 -18
- package/dist/tools/report-session/examine-page.js +204 -24
- package/dist/tools/report-session/index.d.ts +2 -0
- package/dist/tools/report-session/index.js +2 -0
- package/dist/tools/report-session/list-artifacts.d.ts +97 -0
- package/dist/tools/report-session/list-artifacts.js +197 -0
- package/dist/tools/report-session/preview-report.d.ts +6 -0
- package/dist/tools/report-session/preview-report.js +180 -28
- package/dist/tools/report-session/record-version.d.ts +22 -10
- package/dist/tools/report-session/record-version.js +99 -14
- package/dist/tools/start-report-session.d.ts +115 -0
- package/dist/tools/start-report-session.js +179 -0
- package/dist/workflows/sandbox-step.d.ts +7 -4
- package/dist/workflows/sandbox-step.js +10 -12
- package/dist/workspace/paths.d.ts +50 -0
- package/dist/workspace/paths.js +63 -0
- package/package.json +2 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool that starts a report session from a conversation.
|
|
3
|
+
*
|
|
4
|
+
* The tool is the one report path of the conversation agent. It reads the parent
|
|
5
|
+
* thread from the scope of the call, thus the agent cannot name a different
|
|
6
|
+
* conversation. It carries the intent brief of the ask, and the spawn seeds the
|
|
7
|
+
* context of the child with that brief.
|
|
8
|
+
*
|
|
9
|
+
* The order of the checks is fixed. The eyes gate runs first. A composition
|
|
10
|
+
* that names no route to a look is a permanent condition, and the advice is
|
|
11
|
+
* transient state. The advice runs next. At one user turn or less of the parent
|
|
12
|
+
* past the anchor of the newest report child, the tool starts nothing and it
|
|
13
|
+
* names that child. The spawn runs last.
|
|
14
|
+
*
|
|
15
|
+
* Each degraded condition is a typed outcome in the ok channel: a scope with no
|
|
16
|
+
* thread id, a composition with no eyes, the advice, each refusal of the spawn,
|
|
17
|
+
* and a store fault. The tool never throws for one of them.
|
|
18
|
+
*/
|
|
19
|
+
import type { Pool } from "pg";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import type { EnsureSessionStateResult } from "../app/report-session-runtime.js";
|
|
22
|
+
import type { ChromeConfig } from "../lib/chrome.js";
|
|
23
|
+
import type { AcquireEyes } from "../lib/eyes.js";
|
|
24
|
+
import type { Logger } from "../lib/logger.js";
|
|
25
|
+
import type { ThreadType } from "../memory/thread-store.js";
|
|
26
|
+
import { type Tool } from "./define-tool.js";
|
|
27
|
+
/**
|
|
28
|
+
* The brief of the ask, plus the one override of the advice.
|
|
29
|
+
*
|
|
30
|
+
* Each brief field is short prose. No field names a path, a dataset, or a
|
|
31
|
+
* format, because the report session reads those from the workspace itself.
|
|
32
|
+
*/
|
|
33
|
+
declare const startReportSessionInput: z.ZodObject<{
|
|
34
|
+
objective: z.ZodString;
|
|
35
|
+
audience: z.ZodString;
|
|
36
|
+
angle: z.ZodString;
|
|
37
|
+
exclusions: z.ZodOptional<z.ZodString>;
|
|
38
|
+
openQuestions: z.ZodOptional<z.ZodString>;
|
|
39
|
+
newSessionAnyway: z.ZodOptional<z.ZodBoolean>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
export type StartReportSessionInput = z.infer<typeof startReportSessionInput>;
|
|
42
|
+
/**
|
|
43
|
+
* The typed outcome of the tool. Each arm is ok-channel data, thus the tool
|
|
44
|
+
* never throws for a degraded condition.
|
|
45
|
+
*
|
|
46
|
+
* `started` and `existing-session` name a report thread. `refused` names a call
|
|
47
|
+
* whose scope carries no conversation thread. The four refusals of the spawn
|
|
48
|
+
* keep their own names, thus the agent reads one reason from the spawn and from
|
|
49
|
+
* the tool alike. `failed` is a store fault, and it carries a short line.
|
|
50
|
+
*/
|
|
51
|
+
export type StartReportSessionResult = {
|
|
52
|
+
outcome: "started";
|
|
53
|
+
threadId: string;
|
|
54
|
+
title: string | null;
|
|
55
|
+
} | {
|
|
56
|
+
outcome: "existing-session";
|
|
57
|
+
threadId: string;
|
|
58
|
+
title: string | null;
|
|
59
|
+
} | {
|
|
60
|
+
outcome: "refused";
|
|
61
|
+
detail: string;
|
|
62
|
+
} | {
|
|
63
|
+
outcome: "no_browser";
|
|
64
|
+
detail: string;
|
|
65
|
+
} | {
|
|
66
|
+
outcome: "parent_not_found";
|
|
67
|
+
} | {
|
|
68
|
+
outcome: "parent_not_a_conversation";
|
|
69
|
+
threadType: ThreadType;
|
|
70
|
+
} | {
|
|
71
|
+
outcome: "empty_parent_transcript";
|
|
72
|
+
} | {
|
|
73
|
+
outcome: "failed";
|
|
74
|
+
detail: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The construction deps of the tool.
|
|
78
|
+
*
|
|
79
|
+
* The fields build the spawn operation inside the factory. Thus one singleton
|
|
80
|
+
* tool serves every conversation, and it reads the parent from the scope of each
|
|
81
|
+
* call.
|
|
82
|
+
*
|
|
83
|
+
* Two of the fields name a route to a look at the rendered page: `eyes` and a
|
|
84
|
+
* `chrome` config that names a browser. The tool binds no capture seam. That
|
|
85
|
+
* seam replaces the transport of the eyes tool, and the tool here never looks at
|
|
86
|
+
* a page. A composition that binds a capture seam straight to the spawn still
|
|
87
|
+
* opens the gate of that spawn.
|
|
88
|
+
*/
|
|
89
|
+
export interface StartReportSessionToolDeps {
|
|
90
|
+
readonly pool: Pool;
|
|
91
|
+
readonly chrome: ChromeConfig;
|
|
92
|
+
/**
|
|
93
|
+
* The eyes seam of the composition. A bound seam gives a browser for one
|
|
94
|
+
* look, thus the spawn accepts a session with no configured endpoint. The
|
|
95
|
+
* tool passes the seam to the spawn, and it acquires no lease.
|
|
96
|
+
*/
|
|
97
|
+
readonly eyes?: AcquireEyes;
|
|
98
|
+
/**
|
|
99
|
+
* The anchor operation of the report session runtime, which the spawn runs
|
|
100
|
+
* after the seed of the child lands. The composition binds it, thus the tool
|
|
101
|
+
* carries no stale value of its own.
|
|
102
|
+
*/
|
|
103
|
+
readonly anchorSession?: (threadId: string) => Promise<EnsureSessionStateResult>;
|
|
104
|
+
readonly logger?: Logger;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Make the tool that starts a report session, over one Postgres pool and the
|
|
108
|
+
* eyes of the composition.
|
|
109
|
+
*
|
|
110
|
+
* The factory builds the deps of the spawn one time. The spawn and the gate of
|
|
111
|
+
* the tool read that one value, thus the two gates cannot disagree. The tool
|
|
112
|
+
* holds no per-conversation value.
|
|
113
|
+
*/
|
|
114
|
+
export declare function createStartReportSessionTool(deps: StartReportSessionToolDeps): Tool<StartReportSessionInput, StartReportSessionResult>;
|
|
115
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool that starts a report session from a conversation.
|
|
3
|
+
*
|
|
4
|
+
* The tool is the one report path of the conversation agent. It reads the parent
|
|
5
|
+
* thread from the scope of the call, thus the agent cannot name a different
|
|
6
|
+
* conversation. It carries the intent brief of the ask, and the spawn seeds the
|
|
7
|
+
* context of the child with that brief.
|
|
8
|
+
*
|
|
9
|
+
* The order of the checks is fixed. The eyes gate runs first. A composition
|
|
10
|
+
* that names no route to a look is a permanent condition, and the advice is
|
|
11
|
+
* transient state. The advice runs next. At one user turn or less of the parent
|
|
12
|
+
* past the anchor of the newest report child, the tool starts nothing and it
|
|
13
|
+
* names that child. The spawn runs last.
|
|
14
|
+
*
|
|
15
|
+
* Each degraded condition is a typed outcome in the ok channel: a scope with no
|
|
16
|
+
* thread id, a composition with no eyes, the advice, each refusal of the spawn,
|
|
17
|
+
* and a store fault. The tool never throws for one of them.
|
|
18
|
+
*/
|
|
19
|
+
import { ok } from "neverthrow";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { compositionHasEyes, createReportSessionSpawn } from "../app/spawn-report-session.js";
|
|
22
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
23
|
+
import { describeDbError } from "../lib/db-result.js";
|
|
24
|
+
import { defineTool } from "./define-tool.js";
|
|
25
|
+
/**
|
|
26
|
+
* The character bound of each brief field. The whole brief lands in one durable
|
|
27
|
+
* message row, thus the schema holds no unbounded string out of the store.
|
|
28
|
+
*
|
|
29
|
+
* The description caps the brief at approximately 2000 tokens, and a token is
|
|
30
|
+
* about four characters. Thus the five bounds sum to 8000 characters, and the
|
|
31
|
+
* widest brief that the schema admits still meets the cap that the agent reads.
|
|
32
|
+
*
|
|
33
|
+
* The share of each field follows its role. `audience` names one reader, thus a
|
|
34
|
+
* phrase is enough. `objective` and `angle` carry a sentence or two. Each
|
|
35
|
+
* optional field carries a list of items, thus it takes the largest share.
|
|
36
|
+
*/
|
|
37
|
+
const BRIEF_MAX = {
|
|
38
|
+
objective: 1500,
|
|
39
|
+
audience: 200,
|
|
40
|
+
angle: 1500,
|
|
41
|
+
exclusions: 2400,
|
|
42
|
+
openQuestions: 2400,
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The brief of the ask, plus the one override of the advice.
|
|
46
|
+
*
|
|
47
|
+
* Each brief field is short prose. No field names a path, a dataset, or a
|
|
48
|
+
* format, because the report session reads those from the workspace itself.
|
|
49
|
+
*/
|
|
50
|
+
const startReportSessionInput = z.object({
|
|
51
|
+
objective: z.string().min(1).max(BRIEF_MAX.objective).describe("The question that the report must answer."),
|
|
52
|
+
audience: z.string().min(1).max(BRIEF_MAX.audience).describe("The reader of the report."),
|
|
53
|
+
angle: z.string().min(1).max(BRIEF_MAX.angle).describe("The line that the report takes through the evidence."),
|
|
54
|
+
exclusions: z.string().max(BRIEF_MAX.exclusions).optional().describe("The material that the report must leave out."),
|
|
55
|
+
openQuestions: z.string().max(BRIEF_MAX.openQuestions).optional().describe("The points that the user did not decide yet."),
|
|
56
|
+
newSessionAnyway: z
|
|
57
|
+
.boolean()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Set it to true to start a new session when the conversation holds no user turn of new work after the last report session. The default is false."),
|
|
60
|
+
});
|
|
61
|
+
/** The line that a call reads when the scope names no conversation thread. */
|
|
62
|
+
const NO_THREAD_SCOPE_DETAIL = "the scope of the call names no conversation thread, thus the tool cannot start a child session";
|
|
63
|
+
/** The line that a call reads when the thread store refuses the shape of the child. */
|
|
64
|
+
const STORE_REFUSED_DETAIL = "the thread store refused the shape of the child session";
|
|
65
|
+
/**
|
|
66
|
+
* Map a fault of the spawn onto its arm. The switch is exhaustive over the three
|
|
67
|
+
* error unions, thus a new variant fails the build instead of falling into one
|
|
68
|
+
* arm in silence.
|
|
69
|
+
*/
|
|
70
|
+
function toOutcome(fault) {
|
|
71
|
+
switch (fault.type) {
|
|
72
|
+
case "no_browser":
|
|
73
|
+
return { outcome: "no_browser", detail: fault.detail };
|
|
74
|
+
case "parent_not_found":
|
|
75
|
+
return { outcome: "parent_not_found" };
|
|
76
|
+
case "parent_not_a_conversation":
|
|
77
|
+
return { outcome: "parent_not_a_conversation", threadType: fault.threadType };
|
|
78
|
+
case "empty_parent_transcript":
|
|
79
|
+
return { outcome: "empty_parent_transcript" };
|
|
80
|
+
case "query_failed":
|
|
81
|
+
case "mutation_failed":
|
|
82
|
+
case "connection_failed":
|
|
83
|
+
case "constraint_violation":
|
|
84
|
+
return { outcome: "failed", detail: describeDbError(fault) };
|
|
85
|
+
case "parent_analysis_mismatch":
|
|
86
|
+
case "parent_anchor_unpaired":
|
|
87
|
+
case "unknown_thread_type":
|
|
88
|
+
return { outcome: "failed", detail: STORE_REFUSED_DETAIL };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The cause of a fault, or `undefined` for a fault that carries none. */
|
|
92
|
+
function causeOf(fault) {
|
|
93
|
+
return "cause" in fault ? fault.cause : undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Make the tool that starts a report session, over one Postgres pool and the
|
|
97
|
+
* eyes of the composition.
|
|
98
|
+
*
|
|
99
|
+
* The factory builds the deps of the spawn one time. The spawn and the gate of
|
|
100
|
+
* the tool read that one value, thus the two gates cannot disagree. The tool
|
|
101
|
+
* holds no per-conversation value.
|
|
102
|
+
*/
|
|
103
|
+
export function createStartReportSessionTool(deps) {
|
|
104
|
+
const logger = (deps.logger ?? createNoopLogger()).named("start-report-session");
|
|
105
|
+
const spawnDeps = {
|
|
106
|
+
pool: deps.pool,
|
|
107
|
+
chrome: deps.chrome,
|
|
108
|
+
...(deps.eyes ? { eyes: deps.eyes } : {}),
|
|
109
|
+
...(deps.anchorSession ? { anchorSession: deps.anchorSession } : {}),
|
|
110
|
+
...(deps.logger ? { logger: deps.logger } : {}),
|
|
111
|
+
};
|
|
112
|
+
const spawn = createReportSessionSpawn(spawnDeps);
|
|
113
|
+
// The gate reads the routes of the very spawn below, thus the two answers
|
|
114
|
+
// come from one value. The routes are fixed at construction, thus the gate
|
|
115
|
+
// reads one boolean and never a live probe of the sidecar.
|
|
116
|
+
const eyesAvailable = compositionHasEyes(spawnDeps);
|
|
117
|
+
return defineTool({
|
|
118
|
+
id: "start_report_session",
|
|
119
|
+
description: "Start an interactive report session as a child of this conversation. " +
|
|
120
|
+
"The user composes the report in that chat, with the report agent. " +
|
|
121
|
+
"Give the intent brief: the objective, the audience, and the angle of the report. " +
|
|
122
|
+
"The brief carries intent only. Do not name a path, a dataset, or a format in it, " +
|
|
123
|
+
"because the session reads those from the workspace. " +
|
|
124
|
+
"Keep the whole brief under approximately 2000 tokens. " +
|
|
125
|
+
"When the conversation holds no user turn of new work after the last report session, the tool " +
|
|
126
|
+
"starts nothing and it names that session. The ask that started that session is not new work. " +
|
|
127
|
+
"Then tell the user to continue in that chat.",
|
|
128
|
+
inputSchema: startReportSessionInput,
|
|
129
|
+
executionMode: "inline",
|
|
130
|
+
describeCall: "none",
|
|
131
|
+
execute: async (input, ctx) => {
|
|
132
|
+
const brief = {
|
|
133
|
+
objective: input.objective,
|
|
134
|
+
audience: input.audience,
|
|
135
|
+
angle: input.angle,
|
|
136
|
+
exclusions: input.exclusions,
|
|
137
|
+
openQuestions: input.openQuestions,
|
|
138
|
+
};
|
|
139
|
+
const { scope } = ctx.session;
|
|
140
|
+
if (scope.kind !== "analysis" || scope.threadId === undefined || scope.threadId.length === 0) {
|
|
141
|
+
return ok({ outcome: "refused", detail: NO_THREAD_SCOPE_DETAIL });
|
|
142
|
+
}
|
|
143
|
+
const parentThreadId = scope.threadId;
|
|
144
|
+
// The eyes gate has priority over the advice, and the advice costs two
|
|
145
|
+
// reads. The spawn holds the same gate before each read of its own,
|
|
146
|
+
// thus a blind composition reaches the `no_browser` refusal of the
|
|
147
|
+
// spawn with no read and no write. As a result the tool copies no
|
|
148
|
+
// detail line of the spawn.
|
|
149
|
+
if (eyesAvailable) {
|
|
150
|
+
const delta = await spawn.reportSessionDelta(parentThreadId);
|
|
151
|
+
if (delta.isErr()) {
|
|
152
|
+
logger.warn("the report session delta did not read", {
|
|
153
|
+
parentThreadId,
|
|
154
|
+
reason: delta.error.type,
|
|
155
|
+
...logger.errorFields(delta.error.cause),
|
|
156
|
+
});
|
|
157
|
+
return ok(toOutcome(delta.error));
|
|
158
|
+
}
|
|
159
|
+
const { newestChild, userTurnsSinceAnchor } = delta.value;
|
|
160
|
+
// The rule admits one turn. A turn appends after its own loop
|
|
161
|
+
// runs, thus the ask that made the child is itself one user turn
|
|
162
|
+
// past the anchor. A rule at zero would never advise again after
|
|
163
|
+
// that turn commits. A second user turn is real work, and a new
|
|
164
|
+
// session can report on it.
|
|
165
|
+
if (input.newSessionAnyway !== true && newestChild !== null && userTurnsSinceAnchor !== null && userTurnsSinceAnchor <= 1) {
|
|
166
|
+
return ok({ outcome: "existing-session", threadId: newestChild.threadId, title: newestChild.title });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const spawned = await spawn.spawnReportSession(parentThreadId, brief);
|
|
170
|
+
return ok(spawned.match((child) => ({ outcome: "started", threadId: child.threadId, title: child.title }), (fault) => {
|
|
171
|
+
const outcome = toOutcome(fault);
|
|
172
|
+
if (outcome.outcome === "failed") {
|
|
173
|
+
logger.warn("the report session did not start", { parentThreadId, reason: fault.type, ...logger.errorFields(causeOf(fault)) });
|
|
174
|
+
}
|
|
175
|
+
return outcome;
|
|
176
|
+
}));
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
import type { TokenUsageRollup } from "@inflexa-ai/harness/contracts/usage.js";
|
|
30
30
|
import type { Pool } from "pg";
|
|
31
31
|
import type { Logger } from "../lib/logger.js";
|
|
32
|
-
import type { ResolveBilling } from "../billing/resolver.js";
|
|
33
32
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
34
33
|
import type { CitationResolver } from "../citations/types.js";
|
|
35
34
|
import type { AgentDefinition, LoopMessage } from "../loop/types.js";
|
|
@@ -233,9 +232,13 @@ export interface SandboxStepDeps {
|
|
|
233
232
|
/** Write-side embedder for the post-step vector index. */
|
|
234
233
|
readonly embedding: EmbeddingProvider;
|
|
235
234
|
readonly sandboxClient: SandboxClient;
|
|
236
|
-
/**
|
|
237
|
-
*
|
|
238
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Host-supplied labels for this step's sandbox pod, resolved under the
|
|
237
|
+
* step's session. The map is opaque to the harness: it stamps each entry
|
|
238
|
+
* and interprets none of it. Absent in a wiring that attributes nothing —
|
|
239
|
+
* the pod then carries the harness's own labels only.
|
|
240
|
+
*/
|
|
241
|
+
readonly resolvePodLabels?: (session: RunSession) => Promise<Record<string, string>>;
|
|
239
242
|
/**
|
|
240
243
|
* External artifact registration + sync seam. The harness's post-step pipeline
|
|
241
244
|
* registers each step's outputs through it (filesystem index in the
|
|
@@ -46,8 +46,6 @@ import { createBlockerHolder } from "../tools/sandbox/report-blocker.js";
|
|
|
46
46
|
* declare it explicitly.
|
|
47
47
|
*/
|
|
48
48
|
export const DEFAULT_STEP_TIMEOUT_SECONDS = 3600;
|
|
49
|
-
/** Resolved-header key carrying the billing-context id (see `ResolveBilling`). */
|
|
50
|
-
const BILLING_CONTEXT_HEADER = "X-Inflexa-Billing-Context";
|
|
51
49
|
/**
|
|
52
50
|
* DBOS message topic the child uses to notify the parent that it is
|
|
53
51
|
* about to self-cancel because of a 402 budget-exceeded error. The
|
|
@@ -153,7 +151,7 @@ export async function runSandboxStepBody(input, deps) {
|
|
|
153
151
|
const identity = await DBOS.runStep(() => Promise.resolve(mintSandboxIdentity(input.runId)), { name: "sandbox.mint" });
|
|
154
152
|
// (2b) sandbox.create — spawn (or adopt) the machine under the minted
|
|
155
153
|
// identity. The handle (secret included) is cached so recovery picks the
|
|
156
|
-
// same machine back up without re-provisioning.
|
|
154
|
+
// same machine back up without re-provisioning. Pod-label resolution lives
|
|
157
155
|
// INSIDE this step (not as its own step) so the child's step sequence is
|
|
158
156
|
// unchanged — in-flight workflows resumed across a deploy replay cleanly.
|
|
159
157
|
const sandboxMeta = {
|
|
@@ -166,20 +164,20 @@ export async function runSandboxStepBody(input, deps) {
|
|
|
166
164
|
resources: input.resources,
|
|
167
165
|
};
|
|
168
166
|
const sandbox = await DBOS.runStep(async () => {
|
|
169
|
-
let
|
|
170
|
-
if (deps.
|
|
167
|
+
let podLabels;
|
|
168
|
+
if (deps.resolvePodLabels) {
|
|
171
169
|
try {
|
|
172
|
-
|
|
173
|
-
if (bcId)
|
|
174
|
-
billing = { billingContextId: bcId, userId: input.runSession.identity.user };
|
|
170
|
+
podLabels = await deps.resolvePodLabels(session);
|
|
175
171
|
}
|
|
176
172
|
catch (err) {
|
|
177
|
-
logger.
|
|
173
|
+
logger.warn("pod-label resolution failed", { analysisId: input.analysisId, ...logger.errorFields(err) });
|
|
178
174
|
}
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
// A wired resolver that yields nothing is the one loud case: the
|
|
176
|
+
// host asked for attribution and the pod spawns without it.
|
|
177
|
+
if (!podLabels || Object.keys(podLabels).length === 0)
|
|
178
|
+
logger.warn("sandbox spawned with no pod labels", { analysisId: input.analysisId });
|
|
181
179
|
}
|
|
182
|
-
return deps.sandboxClient.createSandbox({ ...sandboxMeta,
|
|
180
|
+
return deps.sandboxClient.createSandbox({ ...sandboxMeta, podLabels }, identity);
|
|
183
181
|
}, { name: "sandbox.create" });
|
|
184
182
|
// Recovery path: re-check `isAlive` on the persisted ref before continuing.
|
|
185
183
|
// A classified-dead sandbox triggers a fresh `createSandbox` (task 4.6).
|
|
@@ -122,6 +122,34 @@ export declare function stepWritePrefix(args: {
|
|
|
122
122
|
readonly runId: string;
|
|
123
123
|
readonly stepId: string;
|
|
124
124
|
}): string;
|
|
125
|
+
/**
|
|
126
|
+
* Split a declared sandbox write tail into its segments, and refuse a tail that is
|
|
127
|
+
* not one workspace-relative path.
|
|
128
|
+
*
|
|
129
|
+
* A tail names the one writable mount of a sandbox in place of the step directory.
|
|
130
|
+
* It reaches the mount builders from a caller, thus it takes the same discipline as
|
|
131
|
+
* a `stepId`: each segment passes {@link assertSafeId}, which refuses a separator,
|
|
132
|
+
* a NUL, a shell-hostile char, and a pure-dot segment. An empty tail names the
|
|
133
|
+
* workspace root itself, and an absolute tail names a host location. Both are
|
|
134
|
+
* refused, because a write mount must be a directory inside the tree.
|
|
135
|
+
*
|
|
136
|
+
* The segments come back so a host-path builder joins the validated parts, and it
|
|
137
|
+
* never re-splits the raw text.
|
|
138
|
+
*/
|
|
139
|
+
export declare function assertSafeTail(tail: string, label?: string): string[];
|
|
140
|
+
/**
|
|
141
|
+
* Derive the host path of a declared write tail under a workspace root.
|
|
142
|
+
*
|
|
143
|
+
* The counterpart of {@link stepWritePrefix} for a sandbox that declares its own
|
|
144
|
+
* write tail. The tail passes the same validation before it becomes a host
|
|
145
|
+
* directory path, thus a crafted segment cannot widen or escape the mount. This is
|
|
146
|
+
* the one builder of that path: the host makes the directory with it, and the
|
|
147
|
+
* Docker bind source comes from it.
|
|
148
|
+
*/
|
|
149
|
+
export declare function tailWritePrefix(args: {
|
|
150
|
+
readonly workspaceRoot: string;
|
|
151
|
+
readonly tail: string;
|
|
152
|
+
}): string;
|
|
125
153
|
/**
|
|
126
154
|
* Map a host-side absolute path within an analysis's workspace tree to its
|
|
127
155
|
* in-sandbox absolute path. The tree is bind-mounted at `/{resourceId}`, so
|
|
@@ -152,6 +180,28 @@ export declare function previewDir(previewId: string): string;
|
|
|
152
180
|
* Expects names like "v1", "v2", etc. Returns 0 if no versions found.
|
|
153
181
|
*/
|
|
154
182
|
export declare function latestPreviewVersion(dirNames: string[]): number;
|
|
183
|
+
/**
|
|
184
|
+
* Parent of every report session, workspace-root-relative. Callers that
|
|
185
|
+
* enumerate the sessions join this. Callers that address one join
|
|
186
|
+
* {@link reportSessionDir}. Both spell the segment through this constant, thus
|
|
187
|
+
* the tree has one owner.
|
|
188
|
+
*/
|
|
189
|
+
export declare const REPORT_SESSIONS_ROOT = "report-sessions";
|
|
190
|
+
/**
|
|
191
|
+
* Directory of one report session, workspace-root-relative. The page of the
|
|
192
|
+
* session and its staged assets live under this one directory. Thus a host that
|
|
193
|
+
* reclaims the files of a purged session names each directory through this
|
|
194
|
+
* helper. The host restates no layout of the harness.
|
|
195
|
+
*/
|
|
196
|
+
export declare function reportSessionDir(threadId: string): string;
|
|
197
|
+
/**
|
|
198
|
+
* Directory of the derived tables of one report session, workspace-root-relative.
|
|
199
|
+
*
|
|
200
|
+
* The derivation writes its output here, and the record prunes an output that the recorded document does
|
|
201
|
+
* not name. Both spell the segment through this helper, thus the write mount and the prune scope cannot
|
|
202
|
+
* disagree.
|
|
203
|
+
*/
|
|
204
|
+
export declare function reportSessionDerivedDir(threadId: string): string;
|
|
155
205
|
/**
|
|
156
206
|
* True when `value` is a safe single path segment: the `SAFE_ID` charset (word
|
|
157
207
|
* chars, `.`, `-` — no slash, NUL, or shell-hostile char) AND not a pure-dot
|
package/dist/workspace/paths.js
CHANGED
|
@@ -96,6 +96,42 @@ export function stepWritePrefix(args) {
|
|
|
96
96
|
assertSafeId(args.stepId, "stepId");
|
|
97
97
|
return resolvePath(args.workspaceRoot, "runs", args.runId, args.stepId);
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Split a declared sandbox write tail into its segments, and refuse a tail that is
|
|
101
|
+
* not one workspace-relative path.
|
|
102
|
+
*
|
|
103
|
+
* A tail names the one writable mount of a sandbox in place of the step directory.
|
|
104
|
+
* It reaches the mount builders from a caller, thus it takes the same discipline as
|
|
105
|
+
* a `stepId`: each segment passes {@link assertSafeId}, which refuses a separator,
|
|
106
|
+
* a NUL, a shell-hostile char, and a pure-dot segment. An empty tail names the
|
|
107
|
+
* workspace root itself, and an absolute tail names a host location. Both are
|
|
108
|
+
* refused, because a write mount must be a directory inside the tree.
|
|
109
|
+
*
|
|
110
|
+
* The segments come back so a host-path builder joins the validated parts, and it
|
|
111
|
+
* never re-splits the raw text.
|
|
112
|
+
*/
|
|
113
|
+
export function assertSafeTail(tail, label = "writableTail") {
|
|
114
|
+
if (tail.length === 0 || tail.startsWith("/")) {
|
|
115
|
+
throw new Error(`Invalid ${label}: ${tail}`);
|
|
116
|
+
}
|
|
117
|
+
const segments = tail.split("/");
|
|
118
|
+
for (const segment of segments) {
|
|
119
|
+
assertSafeId(segment, label);
|
|
120
|
+
}
|
|
121
|
+
return segments;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Derive the host path of a declared write tail under a workspace root.
|
|
125
|
+
*
|
|
126
|
+
* The counterpart of {@link stepWritePrefix} for a sandbox that declares its own
|
|
127
|
+
* write tail. The tail passes the same validation before it becomes a host
|
|
128
|
+
* directory path, thus a crafted segment cannot widen or escape the mount. This is
|
|
129
|
+
* the one builder of that path: the host makes the directory with it, and the
|
|
130
|
+
* Docker bind source comes from it.
|
|
131
|
+
*/
|
|
132
|
+
export function tailWritePrefix(args) {
|
|
133
|
+
return resolvePath(args.workspaceRoot, ...assertSafeTail(args.tail));
|
|
134
|
+
}
|
|
99
135
|
/**
|
|
100
136
|
* Map a host-side absolute path within an analysis's workspace tree to its
|
|
101
137
|
* in-sandbox absolute path. The tree is bind-mounted at `/{resourceId}`, so
|
|
@@ -152,6 +188,33 @@ export function latestPreviewVersion(dirNames) {
|
|
|
152
188
|
}
|
|
153
189
|
return max;
|
|
154
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Parent of every report session, workspace-root-relative. Callers that
|
|
193
|
+
* enumerate the sessions join this. Callers that address one join
|
|
194
|
+
* {@link reportSessionDir}. Both spell the segment through this constant, thus
|
|
195
|
+
* the tree has one owner.
|
|
196
|
+
*/
|
|
197
|
+
export const REPORT_SESSIONS_ROOT = "report-sessions";
|
|
198
|
+
/**
|
|
199
|
+
* Directory of one report session, workspace-root-relative. The page of the
|
|
200
|
+
* session and its staged assets live under this one directory. Thus a host that
|
|
201
|
+
* reclaims the files of a purged session names each directory through this
|
|
202
|
+
* helper. The host restates no layout of the harness.
|
|
203
|
+
*/
|
|
204
|
+
export function reportSessionDir(threadId) {
|
|
205
|
+
assertSafeId(threadId, "threadId");
|
|
206
|
+
return `${REPORT_SESSIONS_ROOT}/${threadId}`;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Directory of the derived tables of one report session, workspace-root-relative.
|
|
210
|
+
*
|
|
211
|
+
* The derivation writes its output here, and the record prunes an output that the recorded document does
|
|
212
|
+
* not name. Both spell the segment through this helper, thus the write mount and the prune scope cannot
|
|
213
|
+
* disagree.
|
|
214
|
+
*/
|
|
215
|
+
export function reportSessionDerivedDir(threadId) {
|
|
216
|
+
return `${reportSessionDir(threadId)}/derived`;
|
|
217
|
+
}
|
|
155
218
|
/**
|
|
156
219
|
* For an analysis-rooted input (`/{analysisId}/...`), strip the leading
|
|
157
220
|
* `/{analysisId}/` segment so it can be resolved relative to the analysis
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inflexa-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Host-agnostic bioinformatics agent harness \u2014 hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
65
65
|
"@opentelemetry/sdk-trace-node": "^2.9.0",
|
|
66
66
|
"@opentelemetry/semantic-conventions": "^1.43.0",
|
|
67
|
+
"ag-grid-community": "36.1.0",
|
|
67
68
|
"ai": "^7.0.61",
|
|
68
69
|
"cheerio": "^1.2.0",
|
|
69
70
|
"dockerode": "^4.0.12",
|