@inflexa-ai/harness 0.12.0 → 0.14.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 +9 -15
- package/dist/agents/conversation-agent.js +32 -31
- package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
- package/dist/agents/sandbox/chromatin-agent.js +1 -1
- package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
- package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
- package/dist/agents/sandbox/enrichment-agent.js +1 -1
- package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
- package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
- package/dist/agents/sandbox/index.js +0 -3
- package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
- package/dist/agents/sandbox/network-agent.js +1 -1
- package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
- package/dist/agents/sandbox/scientific-executor.js +1 -1
- package/dist/agents/sandbox/shared.d.ts +2 -2
- package/dist/agents/sandbox/shared.js +8 -14
- package/dist/agents/sandbox/translational-safety-agent.js +4 -7
- package/dist/agents/sandbox/types.d.ts +5 -5
- package/dist/agents/sandbox/types.js +4 -4
- package/dist/app/chat-turn.js +4 -1
- package/dist/app/message-assembly.d.ts +5 -2
- package/dist/app/message-assembly.js +7 -2
- package/dist/app/run-activity.d.ts +13 -0
- package/dist/app/run-activity.js +51 -0
- package/dist/config/resource-limits.d.ts +0 -7
- package/dist/config/resource-limits.js +0 -1
- package/dist/contracts/target-dossier.d.ts +10 -10
- package/dist/execution/dbos-run-launcher.js +1 -26
- package/dist/execution/report-runner.js +1 -1
- package/dist/execution/run-authorizer.d.ts +1 -1
- package/dist/execution/run-authorizer.js +1 -1
- package/dist/execution/run-event-parts.d.ts +44 -0
- package/dist/execution/run-event-parts.js +0 -0
- package/dist/execution/run-event-stream.d.ts +91 -0
- package/dist/execution/run-event-stream.js +212 -0
- package/dist/execution/run-launcher.d.ts +2 -18
- package/dist/execution/run-launcher.js +2 -4
- package/dist/execution/run-synthesis.js +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +23 -6
- package/dist/input-sanitization.d.ts +14 -4
- package/dist/input-sanitization.js +27 -4
- package/dist/lib/storage.js +4 -1
- package/dist/loop/budget-exceeded.d.ts +18 -2
- package/dist/loop/budget-exceeded.js +49 -2
- package/dist/loop/run-agent.d.ts +22 -1
- package/dist/loop/run-agent.js +51 -5
- package/dist/loop/run-to-terminal.d.ts +0 -2
- package/dist/loop/run-to-terminal.js +8 -1
- package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
- package/dist/memory/ai-sdk-message-storage.js +45 -4
- package/dist/memory/content-to-cortex.js +19 -3
- package/dist/memory/message-backfill.js +1 -1
- package/dist/memory/reconstruct-cards.d.ts +1 -1
- package/dist/memory/reconstruct-cards.js +7 -5
- package/dist/memory/thread-history.js +72 -8
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +41 -15
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +16 -14
- package/dist/prompts/planner.d.ts +3 -0
- package/dist/prompts/planner.js +16 -21
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
- package/dist/prompts/sandbox-standards.d.ts +4 -4
- package/dist/prompts/sandbox-standards.js +5 -5
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +7 -7
- package/dist/providers/errors.d.ts +7 -0
- package/dist/providers/errors.js +88 -3
- package/dist/runtime/assemble.d.ts +5 -10
- package/dist/runtime/assemble.js +0 -4
- package/dist/runtime/boot.d.ts +2 -2
- package/dist/runtime/boot.js +1 -1
- package/dist/runtime/dbos.d.ts +4 -5
- package/dist/runtime/dbos.js +6 -7
- package/dist/runtime/pools.d.ts +15 -1
- package/dist/runtime/pools.js +15 -1
- package/dist/sandbox/mount-plan.d.ts +1 -1
- package/dist/sandbox/sandbox-error.d.ts +1 -1
- package/dist/sandbox/sandbox-error.js +1 -1
- package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
- package/dist/sandbox/sandbox-step-translate.js +17 -2
- package/dist/sandbox/types.d.ts +1 -1
- package/dist/schemas/validate-plan.d.ts +1 -1
- package/dist/schemas/workflow-state.d.ts +2 -2
- package/dist/schemas/workflow-state.js +2 -2
- package/dist/state/data-profile.d.ts +53 -1
- package/dist/state/data-profile.js +66 -6
- package/dist/state/index.d.ts +6 -6
- package/dist/state/index.js +4 -4
- package/dist/state/init.js +37 -2
- package/dist/state/plans.js +1 -1
- package/dist/state/runs.d.ts +49 -0
- package/dist/state/runs.js +150 -0
- package/dist/tasks/data-profile-activity.d.ts +87 -0
- package/dist/tasks/data-profile-activity.js +56 -0
- package/dist/tasks/data-profile.js +66 -4
- package/dist/tools/__fixtures__/tool-context.js +1 -0
- package/dist/tools/ad-hoc-router.d.ts +31 -0
- package/dist/tools/ad-hoc-router.js +132 -0
- package/dist/tools/analysis-invocation.d.ts +4 -0
- package/dist/tools/analysis-invocation.js +17 -0
- package/dist/tools/bio/chembl.d.ts +1 -1
- package/dist/tools/bio/chembl.js +51 -54
- package/dist/tools/bio/comptox.js +35 -33
- package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
- package/dist/tools/bio/drug-gene-interactions.js +175 -0
- package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
- package/dist/tools/bio/gene-disease-evidence.js +187 -0
- package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
- package/dist/tools/bio/gene-preclinical-profile.js +125 -0
- package/dist/tools/bio/index.d.ts +23 -16
- package/dist/tools/bio/index.js +29 -22
- package/dist/tools/bio/keys.d.ts +16 -9
- package/dist/tools/bio/keys.js +22 -13
- package/dist/tools/bio/lookup-annotation.d.ts +48 -0
- package/dist/tools/bio/lookup-annotation.js +173 -0
- package/dist/tools/bio/opentargets.d.ts +7 -11
- package/dist/tools/bio/opentargets.js +24 -35
- package/dist/tools/bio/pubchem.js +6 -4
- package/dist/tools/bio/pubmed.d.ts +15 -7
- package/dist/tools/bio/pubmed.js +79 -20
- package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
- package/dist/tools/bio/search-clinical-trials.js +26 -5
- package/dist/tools/bio/search-faers.js +9 -3
- package/dist/tools/bio/search-gene.d.ts +0 -1
- package/dist/tools/bio/search-gene.js +4 -5
- package/dist/tools/bio/search-geo-datasets.js +10 -4
- package/dist/tools/bio/search-interactions.d.ts +8 -4
- package/dist/tools/bio/search-interactions.js +28 -12
- package/dist/tools/bio/target-safety.d.ts +59 -0
- package/dist/tools/bio/target-safety.js +191 -0
- package/dist/tools/define-tool.d.ts +6 -2
- package/dist/tools/define-tool.js +1 -1
- package/dist/tools/execute-analysis.d.ts +44 -0
- package/dist/tools/execute-analysis.js +252 -0
- package/dist/tools/iterate-report.js +7 -6
- package/dist/tools/lib/dgidb-client.d.ts +49 -0
- package/dist/tools/lib/dgidb-client.js +168 -0
- package/dist/tools/lib/disgenet-client.d.ts +56 -0
- package/dist/tools/lib/disgenet-client.js +59 -0
- package/dist/tools/lib/drugbank-client.d.ts +83 -0
- package/dist/tools/lib/drugbank-client.js +95 -0
- package/dist/tools/lib/ensembl-client.d.ts +4 -1
- package/dist/tools/lib/ensembl-client.js +5 -3
- package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
- package/dist/tools/lib/gwas-catalog-client.js +136 -0
- package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
- package/dist/tools/lib/pharmgkb-client.js +32 -0
- package/dist/tools/lib/pubmed-client.d.ts +57 -4
- package/dist/tools/lib/pubmed-client.js +57 -3
- package/dist/tools/lib/string-client.d.ts +25 -4
- package/dist/tools/lib/string-client.js +22 -5
- package/dist/tools/research/generate-plan.d.ts +16 -9
- package/dist/tools/research/generate-plan.js +116 -70
- package/dist/tools/research/inspect-data-profile.d.ts +31 -6
- package/dist/tools/research/inspect-data-profile.js +23 -7
- package/dist/tools/research/inspect-run.d.ts +33 -15
- package/dist/tools/research/inspect-run.js +155 -66
- package/dist/tools/research/literature-reviewer.js +7 -11
- package/dist/workflows/execute-analysis.d.ts +68 -5
- package/dist/workflows/execute-analysis.js +76 -7
- package/dist/workflows/sandbox-step.js +1 -11
- package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
- package/package.json +1 -1
- package/dist/agents/sandbox/ephemeral-executor.d.ts +0 -9
- package/dist/agents/sandbox/ephemeral-executor.js +0 -20
- package/dist/execution/ephemeral-runner.d.ts +0 -98
- package/dist/execution/ephemeral-runner.js +0 -189
- package/dist/prompts/sandbox/ephemeral-executor.d.ts +0 -1
- package/dist/prompts/sandbox/ephemeral-executor.js +0 -66
- package/dist/tools/bio/check-safety-panel.d.ts +0 -35
- package/dist/tools/bio/check-safety-panel.js +0 -76
- package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
- package/dist/tools/bio/get-impc-ko-profile.js +0 -23
- package/dist/tools/bio/lookup-go-term.d.ts +0 -31
- package/dist/tools/bio/lookup-go-term.js +0 -116
- package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
- package/dist/tools/bio/search-bgee-expression.js +0 -30
- package/dist/tools/bio/search-clinvar.d.ts +0 -13
- package/dist/tools/bio/search-clinvar.js +0 -28
- package/dist/tools/bio/search-dgidb.d.ts +0 -53
- package/dist/tools/bio/search-dgidb.js +0 -213
- package/dist/tools/bio/search-disgenet.d.ts +0 -30
- package/dist/tools/bio/search-disgenet.js +0 -84
- package/dist/tools/bio/search-drugbank.d.ts +0 -38
- package/dist/tools/bio/search-drugbank.js +0 -109
- package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
- package/dist/tools/bio/search-gwas-catalog.js +0 -172
- package/dist/tools/bio/search-pathway.d.ts +0 -12
- package/dist/tools/bio/search-pathway.js +0 -32
- package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
- package/dist/tools/bio/search-pharmgkb.js +0 -52
- package/dist/tools/execute-plan.d.ts +0 -64
- package/dist/tools/execute-plan.js +0 -211
- package/dist/tools/run-ephemeral.d.ts +0 -33
- package/dist/tools/run-ephemeral.js +0 -65
package/dist/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type { ProvenanceCollector } from "./provenance/collector.js";
|
|
|
23
23
|
export { UnavailablePreviewPublisher } from "./tools/report/preview-publisher.js";
|
|
24
24
|
export type { PreviewPublisher, PreviewMintResult } from "./tools/report/preview-publisher.js";
|
|
25
25
|
export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
|
|
26
|
-
export type { RunLauncher, LaunchOptions
|
|
26
|
+
export type { RunLauncher, LaunchOptions } from "./execution/run-launcher.js";
|
|
27
27
|
export { defineTool, isToolError } from "./tools/define-tool.js";
|
|
28
28
|
export type { Tool, ToolDefinition, ToolContext, ToolError } from "./tools/define-tool.js";
|
|
29
29
|
export { createAskGateway } from "./tools/approval/gateway.js";
|
|
@@ -54,6 +54,7 @@ export { createThreadStore } from "./memory/thread-store.js";
|
|
|
54
54
|
export type { ThreadStore, Thread, CreateThreadInput, ListThreadsInput, ThreadPage } from "./memory/thread-store.js";
|
|
55
55
|
export { createThreadHistory } from "./memory/thread-history.js";
|
|
56
56
|
export type { ThreadHistory, StoredMessage, MessagePage, RetractOutcome } from "./memory/thread-history.js";
|
|
57
|
+
export { syntheticUserMessage, syntheticRecordMessage, isSyntheticUserMessage, isSyntheticRecordMessage } from "./memory/ai-sdk-message-storage.js";
|
|
57
58
|
export { contentToCortexMessages } from "./memory/content-to-cortex.js";
|
|
58
59
|
export { createCardResolver } from "./memory/reconstruct-cards.js";
|
|
59
60
|
export type { ToolCardResolver, StoredToolCallForCard } from "./memory/reconstruct-cards.js";
|
|
@@ -88,9 +89,11 @@ export { deliverExecEvent, execEventTopic } from "./sandbox/deliver-exec-event.j
|
|
|
88
89
|
export { ExecEventMessageSchema } from "./sandbox/types.js";
|
|
89
90
|
export type { ExecEventMessage, DoneMarker } from "./sandbox/types.js";
|
|
90
91
|
export { registerExecuteAnalysis } from "./workflows/execute-analysis.js";
|
|
91
|
-
export type { ExecuteAnalysisDeps, ExecuteAnalysisInput, ExecuteAnalysisResult, RunProvenanceEvent } from "./workflows/execute-analysis.js";
|
|
92
|
+
export type { ExecuteAnalysisDeps, ExecuteAnalysisInput, ExecuteAnalysisResult, RunObservation, RunProvenanceEvent, RunStepObservation, StepRuntimeStatus, } from "./workflows/execute-analysis.js";
|
|
92
93
|
export { registerSandboxStep } from "./workflows/sandbox-step.js";
|
|
93
94
|
export type { SandboxStepDeps, SandboxStepInput, SandboxStepResult, SandboxAgentBuildContext } from "./workflows/sandbox-step.js";
|
|
95
|
+
export { createRunEventStream } from "./execution/run-event-stream.js";
|
|
96
|
+
export type { RunEventStream, RunEventStreamDeps, RunEventSubscribeOptions, RunEventPartHandler } from "./execution/run-event-stream.js";
|
|
94
97
|
export { createSandboxAgents, SANDBOX_AGENT_META } from "./agents/sandbox/index.js";
|
|
95
98
|
export type { SandboxAgentDeps, SandboxStepCoords, AgentMeta } from "./agents/sandbox/index.js";
|
|
96
99
|
export { AnalysisPlanSchema } from "./schemas/workflow-state.js";
|
|
@@ -101,8 +104,8 @@ export type { ValidatePlanOptions } from "./schemas/validate-plan.js";
|
|
|
101
104
|
export type { ValidationResult } from "./schemas/validate-plan.js";
|
|
102
105
|
export { upsertPlan, loadPlan } from "./state/plans.js";
|
|
103
106
|
export type { UpsertPlanInput } from "./state/plans.js";
|
|
104
|
-
export { insertRun, queryActiveRun, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError } from "./state/runs.js";
|
|
105
|
-
export type { InsertRunInput } from "./state/runs.js";
|
|
107
|
+
export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
|
|
108
|
+
export type { InsertRunInput, RunReservation } from "./state/runs.js";
|
|
106
109
|
export { queryStepsByRun } from "./state/step-executions.js";
|
|
107
110
|
export type { CortexRunRow, StepExecutionRow, RunStatus } from "./state/schema.js";
|
|
108
111
|
export type { DbError } from "./lib/db-result.js";
|
package/dist/index.js
CHANGED
|
@@ -81,6 +81,16 @@ export { createStreamingChat } from "./providers/streaming-chat.js";
|
|
|
81
81
|
export { prepareChatTurn } from "./app/chat-turn.js";
|
|
82
82
|
export { createThreadStore } from "./memory/thread-store.js";
|
|
83
83
|
export { createThreadHistory } from "./memory/thread-history.js";
|
|
84
|
+
// The synthetic-message primitives. A host appends a record of out-of-band work (an analysis run's
|
|
85
|
+
// outcome) into a thread through `appendTurn`, and needs a message the model reads but that is not
|
|
86
|
+
// user input — which is exactly what the marker denotes. The constructor is exported rather than
|
|
87
|
+
// the marker key so a host can never hand-assemble a message that the turn-boundary predicates
|
|
88
|
+
// (`isGenuineUserStart` and its SQL twin) would then fail to recognise; `isSyntheticUserMessage`
|
|
89
|
+
// lets a host renderer tell such a message apart from a genuine user turn on the way back out.
|
|
90
|
+
// `syntheticRecordMessage` is the one a HOST reaches for: equally non-turn-opening, but rendered —
|
|
91
|
+
// the display reconstruction emits it as a `system` message rather than dropping it as loop
|
|
92
|
+
// machinery, so an appended run outcome is visible in the transcript instead of silently absent.
|
|
93
|
+
export { syntheticUserMessage, syntheticRecordMessage, isSyntheticUserMessage, isSyntheticRecordMessage } from "./memory/ai-sdk-message-storage.js";
|
|
84
94
|
export { contentToCortexMessages } from "./memory/content-to-cortex.js";
|
|
85
95
|
export { createCardResolver } from "./memory/reconstruct-cards.js";
|
|
86
96
|
export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./contracts/part-registry.js";
|
|
@@ -89,11 +99,9 @@ export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./cont
|
|
|
89
99
|
// the staged-input manifest contract, the sandbox/workspace factories over its
|
|
90
100
|
// workspace tree, and the exec-callback envelope pieces (the HTTP→DBOS-topic
|
|
91
101
|
// ingress is the embedder's to host; see the sandbox-server callback protocol).
|
|
92
|
-
// DBOS lifecycle. `sweepEphemeralWorkflows` is a pre-launch
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// for a chat turn that no longer exists). The embedder sweeps it with a direct
|
|
96
|
-
// system-DB UPDATE between state-init and `launchDbos` — the only race-free point.
|
|
102
|
+
// DBOS lifecycle. `sweepEphemeralWorkflows` is a pre-launch upgrade migration:
|
|
103
|
+
// older releases could leave PENDING `ephemeral:*` rows that recovery would
|
|
104
|
+
// otherwise re-dispatch. No current workflow creates that prefix.
|
|
97
105
|
export { launchDbos, shutdownDbos, sweepEphemeralWorkflows } from "./runtime/dbos.js";
|
|
98
106
|
// DBOS workflow-status vocabulary. An embedder that reads its own
|
|
99
107
|
// `dbos.workflow_status` rows must classify them against the SDK's status set,
|
|
@@ -140,6 +148,15 @@ export { ExecEventMessageSchema } from "./sandbox/types.js";
|
|
|
140
148
|
// these, but an embedder running only the run engine registers them directly.
|
|
141
149
|
export { registerExecuteAnalysis } from "./workflows/execute-analysis.js";
|
|
142
150
|
export { registerSandboxStep } from "./workflows/sandbox-step.js";
|
|
151
|
+
// Run-event read seam — the fine-grained durable channel beside `observeRun`.
|
|
152
|
+
// The two are a deliberate pair, not a duplication: `observeRun` is the coarse,
|
|
153
|
+
// in-process run/step snapshot a workflow hands its host synchronously, and it
|
|
154
|
+
// defers sub-step detail to the run's durable event stream. This is that stream,
|
|
155
|
+
// read back — the per-tool-call activity, file trees, and step summaries a run
|
|
156
|
+
// writes, fanned in from the parent workflow and every sandbox-step child.
|
|
157
|
+
// Nothing in the signature belongs to the durability engine, so an embedder
|
|
158
|
+
// consumes run events without depending on it.
|
|
159
|
+
export { createRunEventStream } from "./execution/run-event-stream.js";
|
|
143
160
|
// Sandbox-agent catalog. `buildAgent` maps a `SandboxAgentBuildContext` onto
|
|
144
161
|
// `SandboxAgentDeps`, then selects the per-step agent by id from this record;
|
|
145
162
|
// `SANDBOX_AGENT_META` is the planner-facing meta the same ids key.
|
|
@@ -157,7 +174,7 @@ export { validatePlan } from "./schemas/validate-plan.js";
|
|
|
157
174
|
// caller-derived id and inserts-if-absent (the deterministic-id intake path);
|
|
158
175
|
// the run-state calls mirror the chat trigger's reserve → dedup → status flow.
|
|
159
176
|
export { upsertPlan, loadPlan } from "./state/plans.js";
|
|
160
|
-
export { insertRun, queryActiveRun, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError } from "./state/runs.js";
|
|
177
|
+
export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
|
|
161
178
|
export { queryStepsByRun } from "./state/step-executions.js";
|
|
162
179
|
// Host-neutral reference-data catalog and shared installation receipts. Each
|
|
163
180
|
// artifact names the third-party upstream it is fetched from; embedders own all
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Input sanitization —
|
|
3
|
-
*
|
|
4
|
-
* assistant messages or tool results.
|
|
2
|
+
* Input sanitization — pure, always-on text functions, not a plug-in pipeline.
|
|
3
|
+
* What runs is exactly what is declared here.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
5
|
+
* `normalizeUnicode` and `redactSecrets` are applied once to the incoming user
|
|
6
|
+
* message by the chat route (change 6), and never to assistant messages or tool
|
|
7
|
+
* results. `stripNulCharacters` is the exception — it applies to tool results.
|
|
7
8
|
*/
|
|
8
9
|
/**
|
|
9
10
|
* NFC-normalize and strip control characters.
|
|
@@ -13,6 +14,15 @@
|
|
|
13
14
|
* that could smuggle formatting or terminal escapes.
|
|
14
15
|
*/
|
|
15
16
|
export declare function normalizeUnicode(text: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Strip every NUL from `text`. Applied where a tool result is built — so the
|
|
19
|
+
* loop's in-memory message and the stored row agree byte-for-byte and the
|
|
20
|
+
* prompt-cache prefix survives the round trip — and again where the turn is
|
|
21
|
+
* written, which holds the invariant for writers that are not the loop. User
|
|
22
|
+
* input needs no separate treatment: `normalizeUnicode` already strips NUL along
|
|
23
|
+
* with the rest of the C0 range.
|
|
24
|
+
*/
|
|
25
|
+
export declare function stripNulCharacters(text: string): string;
|
|
16
26
|
/**
|
|
17
27
|
* Replace structured secret formats with `[REDACTED: <name>]`.
|
|
18
28
|
*
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Input sanitization —
|
|
3
|
-
*
|
|
4
|
-
* assistant messages or tool results.
|
|
2
|
+
* Input sanitization — pure, always-on text functions, not a plug-in pipeline.
|
|
3
|
+
* What runs is exactly what is declared here.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
5
|
+
* `normalizeUnicode` and `redactSecrets` are applied once to the incoming user
|
|
6
|
+
* message by the chat route (change 6), and never to assistant messages or tool
|
|
7
|
+
* results. `stripNulCharacters` is the exception — it applies to tool results.
|
|
7
8
|
*/
|
|
8
9
|
/**
|
|
9
10
|
* C0/C1 control characters and DEL, excluding `\t` (U+0009) and `\n`
|
|
@@ -21,6 +22,28 @@ const CONTROL_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
|
|
|
21
22
|
export function normalizeUnicode(text) {
|
|
22
23
|
return text.normalize("NFC").replace(CONTROL_CHARS, "");
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* NUL (U+0000) — the one character a stored message cannot carry. Postgres
|
|
27
|
+
* refuses it inside JSON: a `jsonb` column rejects the insert outright, and a
|
|
28
|
+
* `json` column accepts it but errors the moment a JSON operator reads that row
|
|
29
|
+
* ("U+0000 cannot be converted to text"), which is what `retractLastTurn`'s
|
|
30
|
+
* boundary predicate does. Either way one NUL byte poisons a thread, and it
|
|
31
|
+
* arrives through no fault of the model: tool results carry raw command output
|
|
32
|
+
* and file bytes, which is why they are otherwise left unsanitized.
|
|
33
|
+
*/
|
|
34
|
+
// eslint-disable-next-line no-control-regex -- matching NUL is the intent; see above.
|
|
35
|
+
const NUL = /\u0000/g;
|
|
36
|
+
/**
|
|
37
|
+
* Strip every NUL from `text`. Applied where a tool result is built — so the
|
|
38
|
+
* loop's in-memory message and the stored row agree byte-for-byte and the
|
|
39
|
+
* prompt-cache prefix survives the round trip — and again where the turn is
|
|
40
|
+
* written, which holds the invariant for writers that are not the loop. User
|
|
41
|
+
* input needs no separate treatment: `normalizeUnicode` already strips NUL along
|
|
42
|
+
* with the rest of the C0 range.
|
|
43
|
+
*/
|
|
44
|
+
export function stripNulCharacters(text) {
|
|
45
|
+
return text.replace(NUL, "");
|
|
46
|
+
}
|
|
24
47
|
/**
|
|
25
48
|
* Each entry redacts one structured, prefixed secret format. Every pattern
|
|
26
49
|
* has a prefix (`AKIA`, `sk-`, `gh*_`, `eyJ`, `Bearer`, a DB scheme) that
|
package/dist/lib/storage.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* the composition root and threaded as a constructor dep (see the harness-durable-runtime spec).
|
|
8
8
|
*/
|
|
9
9
|
import pg from "pg";
|
|
10
|
-
import { resolveAppPoolSize } from "../runtime/pools.js";
|
|
10
|
+
import { APP_POOL_ACQUIRE_TIMEOUT_MS, resolveAppPoolSize } from "../runtime/pools.js";
|
|
11
11
|
function poolOptions(config) {
|
|
12
12
|
return {
|
|
13
13
|
host: config.host,
|
|
@@ -18,6 +18,9 @@ function poolOptions(config) {
|
|
|
18
18
|
// Per-pod app pool; DBOS owns its system-DB connections separately
|
|
19
19
|
// (see the postgres-storage-backend spec). The boot guard checks this fits `max_connections`.
|
|
20
20
|
max: resolveAppPoolSize(config.poolMax),
|
|
21
|
+
// Acquisition is bounded so a saturated pool fails a caller instead of
|
|
22
|
+
// hanging it — see `APP_POOL_ACQUIRE_TIMEOUT_MS` for the invariant.
|
|
23
|
+
connectionTimeoutMillis: APP_POOL_ACQUIRE_TIMEOUT_MS,
|
|
21
24
|
// `pg` accepts `ssl: false` to disable entirely, or an object with
|
|
22
25
|
// `rejectUnauthorized` to tune TLS. We map sslmode to those forms.
|
|
23
26
|
ssl: config.sslMode === "disable"
|
|
@@ -6,15 +6,31 @@
|
|
|
6
6
|
* is wasteful because every subsequent call charged to the same budget will
|
|
7
7
|
* also fail until the user tops up.
|
|
8
8
|
*
|
|
9
|
-
* Classification order (first hit wins):
|
|
9
|
+
* Classification order (first hit wins), and the order is load-bearing:
|
|
10
10
|
* 1. Structured `statusCode === 402` (or `status === 402`) on any
|
|
11
11
|
* link of the cause chain. The gateway emits 402 exclusively for
|
|
12
12
|
* `budget_exceeded`, so a structured 402 is unambiguous.
|
|
13
|
-
* 2.
|
|
13
|
+
* 2. A `ProviderError` on the value or its cause chain that was decided on
|
|
14
|
+
* a status: its `type` answers, and the text patterns are skipped.
|
|
15
|
+
* 3. Pattern fallback: `/budget.?exceeded/i` on the stringified
|
|
14
16
|
* top-level error message. Covers stringified throws and paths
|
|
15
17
|
* where no `statusCode` is attached (ops scripts, sandbox-server
|
|
16
18
|
* surfacing upstream billing errors as text).
|
|
17
19
|
*
|
|
20
|
+
* Step 2 exists because the text heuristic must never outrank a real
|
|
21
|
+
* classification. A `ProviderError` message carries an excerpt of the provider's
|
|
22
|
+
* response body, so a non-retryable 400 whose body merely mentions a budget would
|
|
23
|
+
* otherwise match step 3 — and the callers of this predicate act on it fatally
|
|
24
|
+
* (`sandbox-step` self-cancels the workflow, `execute-analysis` re-raises it as a
|
|
25
|
+
* budget throw). A phrase inside a quoted body is not evidence against the status
|
|
26
|
+
* the failure actually carried.
|
|
27
|
+
*
|
|
28
|
+
* "Decided on a status" is the load-bearing qualifier. `toProviderError` wraps
|
|
29
|
+
* every throwable, including ones `classifyProviderError` had no status for — it
|
|
30
|
+
* returns `provider` as a fall-through default there, which is an absence of
|
|
31
|
+
* classification, not a determination. Treating that as authoritative would
|
|
32
|
+
* silence step 3 in precisely the case it was written for.
|
|
33
|
+
*
|
|
18
34
|
* Returns `false` for all other errors. Safe to call with any value;
|
|
19
35
|
* non-Error inputs hit the stringified-pattern branch and return `false`
|
|
20
36
|
* unless the string happens to match.
|
|
@@ -6,19 +6,36 @@
|
|
|
6
6
|
* is wasteful because every subsequent call charged to the same budget will
|
|
7
7
|
* also fail until the user tops up.
|
|
8
8
|
*
|
|
9
|
-
* Classification order (first hit wins):
|
|
9
|
+
* Classification order (first hit wins), and the order is load-bearing:
|
|
10
10
|
* 1. Structured `statusCode === 402` (or `status === 402`) on any
|
|
11
11
|
* link of the cause chain. The gateway emits 402 exclusively for
|
|
12
12
|
* `budget_exceeded`, so a structured 402 is unambiguous.
|
|
13
|
-
* 2.
|
|
13
|
+
* 2. A `ProviderError` on the value or its cause chain that was decided on
|
|
14
|
+
* a status: its `type` answers, and the text patterns are skipped.
|
|
15
|
+
* 3. Pattern fallback: `/budget.?exceeded/i` on the stringified
|
|
14
16
|
* top-level error message. Covers stringified throws and paths
|
|
15
17
|
* where no `statusCode` is attached (ops scripts, sandbox-server
|
|
16
18
|
* surfacing upstream billing errors as text).
|
|
17
19
|
*
|
|
20
|
+
* Step 2 exists because the text heuristic must never outrank a real
|
|
21
|
+
* classification. A `ProviderError` message carries an excerpt of the provider's
|
|
22
|
+
* response body, so a non-retryable 400 whose body merely mentions a budget would
|
|
23
|
+
* otherwise match step 3 — and the callers of this predicate act on it fatally
|
|
24
|
+
* (`sandbox-step` self-cancels the workflow, `execute-analysis` re-raises it as a
|
|
25
|
+
* budget throw). A phrase inside a quoted body is not evidence against the status
|
|
26
|
+
* the failure actually carried.
|
|
27
|
+
*
|
|
28
|
+
* "Decided on a status" is the load-bearing qualifier. `toProviderError` wraps
|
|
29
|
+
* every throwable, including ones `classifyProviderError` had no status for — it
|
|
30
|
+
* returns `provider` as a fall-through default there, which is an absence of
|
|
31
|
+
* classification, not a determination. Treating that as authoritative would
|
|
32
|
+
* silence step 3 in precisely the case it was written for.
|
|
33
|
+
*
|
|
18
34
|
* Returns `false` for all other errors. Safe to call with any value;
|
|
19
35
|
* non-Error inputs hit the stringified-pattern branch and return `false`
|
|
20
36
|
* unless the string happens to match.
|
|
21
37
|
*/
|
|
38
|
+
import { extractStatus, isProviderError } from "../providers/errors.js";
|
|
22
39
|
const BUDGET_EXCEEDED_PATTERNS = [/budget.?exceeded/i];
|
|
23
40
|
/** Max depth walked on the cause chain looking for a structured statusCode. */
|
|
24
41
|
const MAX_CAUSE_HOPS = 5;
|
|
@@ -33,9 +50,39 @@ function hasStatus402(err) {
|
|
|
33
50
|
}
|
|
34
51
|
return false;
|
|
35
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Find a `ProviderError` on the value or its cause chain. A `ResultError`
|
|
55
|
+
* thrown at a step boundary carries the structured value on `.cause`, which is
|
|
56
|
+
* the shape the callers of this predicate actually catch.
|
|
57
|
+
*/
|
|
58
|
+
function findProviderError(err) {
|
|
59
|
+
let cursor = err;
|
|
60
|
+
for (let i = 0; i < MAX_CAUSE_HOPS && cursor; i++) {
|
|
61
|
+
if (isProviderError(cursor))
|
|
62
|
+
return cursor;
|
|
63
|
+
cursor = cursor.cause;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
36
67
|
export function isBudgetExceeded(err) {
|
|
37
68
|
if (hasStatus402(err))
|
|
38
69
|
return true;
|
|
70
|
+
const classified = findProviderError(err);
|
|
71
|
+
if (classified) {
|
|
72
|
+
if (classified.type === "budget")
|
|
73
|
+
return true;
|
|
74
|
+
// `auth` and `tenant-blocked` exist only because a status produced them, and
|
|
75
|
+
// a `provider` arm that carried a status was decided on it. None can be
|
|
76
|
+
// overturned by a phrase inside a response body the message merely quotes.
|
|
77
|
+
//
|
|
78
|
+
// A `provider` arm with NO status is the one exception: there
|
|
79
|
+
// `classifyProviderError` had nothing to key on and applied a fall-through
|
|
80
|
+
// default, so it made no determination to respect. That is exactly the shape
|
|
81
|
+
// a gateway reporting `budget_exceeded` as plain text arrives in, and the
|
|
82
|
+
// patterns below are the reason it is still caught.
|
|
83
|
+
if (classified.type !== "provider" || extractStatus(classified) !== undefined)
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
39
86
|
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
40
87
|
return BUDGET_EXCEEDED_PATTERNS.some((p) => p.test(msg));
|
|
41
88
|
}
|
package/dist/loop/run-agent.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type FinishReason } from "ai";
|
|
2
2
|
import type { AgentSession } from "../auth/types.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Logger } from "../lib/logger.js";
|
|
4
|
+
import type { AgentChat, ChatRequest, PromptCachePolicy } from "../providers/types.js";
|
|
4
5
|
import { type AskApproval, type AskRequest } from "../tools/approval/contract.js";
|
|
5
6
|
import type { AgentDefinition, EmitFn, LoopMessage, RunStep } from "./types.js";
|
|
6
7
|
export interface AgentFinish {
|
|
@@ -34,6 +35,15 @@ export interface RunAgentOptions {
|
|
|
34
35
|
readonly runStep: RunStep;
|
|
35
36
|
readonly formatStepName?: StepNameFormatter;
|
|
36
37
|
readonly isFatalLoopError?: (err: unknown) => boolean;
|
|
38
|
+
/** Tool-selection policy for in-loop model calls. The cap wrap-up remains
|
|
39
|
+
* tool-less regardless. */
|
|
40
|
+
readonly toolChoice?: ChatRequest["toolChoice"];
|
|
41
|
+
/**
|
|
42
|
+
* Optional outcome predicate for loop-driving agents whose result is
|
|
43
|
+
* recorded by a tool into closure state. Checked immediately after each
|
|
44
|
+
* dispatch round, once every sibling tool result has been appended.
|
|
45
|
+
*/
|
|
46
|
+
readonly resolved?: () => boolean;
|
|
37
47
|
/**
|
|
38
48
|
* Prompt-cache policy for every LLM call this run makes. Defaults to
|
|
39
49
|
* `DEFAULT_PROMPT_CACHE` (5m) — an agent loop always re-sends its prefix, so
|
|
@@ -46,6 +56,17 @@ export interface RunAgentOptions {
|
|
|
46
56
|
* cache-write premium for a cache nothing ever reads back.
|
|
47
57
|
*/
|
|
48
58
|
readonly promptCache?: PromptCachePolicy;
|
|
59
|
+
/**
|
|
60
|
+
* Diagnostic sink for the run's own lifecycle. Optional because `runAgent` is
|
|
61
|
+
* called from tools, workflow bodies, and test rigs alike — silence is the
|
|
62
|
+
* correct behaviour for a caller that wires nothing, and the noop fallback
|
|
63
|
+
* gives it without every call site threading `?.`.
|
|
64
|
+
*
|
|
65
|
+
* Distinct from `emit`, which feeds a user-facing surface: every host filters
|
|
66
|
+
* sub-agent events off that surface by `callPath` depth, so a sub-agent's
|
|
67
|
+
* progress is emitted and then dropped. This is where it survives.
|
|
68
|
+
*/
|
|
69
|
+
readonly logger?: Logger;
|
|
49
70
|
}
|
|
50
71
|
export declare function runAgent(agent: AgentDefinition, initial: readonly LoopMessage[], session: AgentSession, opts: RunAgentOptions): Promise<RunAgentResult>;
|
|
51
72
|
export declare function finalText(messages: readonly LoopMessage[]): string;
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsonSchema, tool as aiTool } from "ai";
|
|
2
|
+
import { stripNulCharacters } from "../input-sanitization.js";
|
|
3
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
2
4
|
import { hintForZodIssue, repairToolInput } from "../lib/zod-issues.js";
|
|
3
5
|
import { markInterruptedMessage, syntheticUserMessage } from "../memory/ai-sdk-message-storage.js";
|
|
4
6
|
import { classifyProviderError } from "../providers/errors.js";
|
|
@@ -16,7 +18,11 @@ export const DEFAULT_STEP_NAME_FORMATTER = {
|
|
|
16
18
|
export async function runAgent(agent, initial, session, opts) {
|
|
17
19
|
const { provider, signal, emit, runStep } = opts;
|
|
18
20
|
const formatStepName = opts.formatStepName ?? DEFAULT_STEP_NAME_FORMATTER;
|
|
19
|
-
const
|
|
21
|
+
const configuredFatalLoopError = opts.isFatalLoopError ?? (() => false);
|
|
22
|
+
// AbortError is control flow, not a tool failure. Always compose it with the
|
|
23
|
+
// host's fatal predicate so request cancellation cannot be converted into a
|
|
24
|
+
// model-visible error result and retried by the loop.
|
|
25
|
+
const isFatalLoopError = (err) => ((err instanceof Error || err instanceof DOMException) && err.name === "AbortError") || configuredFatalLoopError(err);
|
|
20
26
|
if (agent.tools.length > 0 && !provider.capabilities.toolCalling) {
|
|
21
27
|
throw new Error(`Provider/model cannot run tool-required agent "${agent.id}"`);
|
|
22
28
|
}
|
|
@@ -25,6 +31,11 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
25
31
|
agentId: session.provenance.agentId ?? agent.id,
|
|
26
32
|
callPath: session.provenance.callPath,
|
|
27
33
|
};
|
|
34
|
+
// Bound from the same `source` the emitted events carry: one derivation feeding
|
|
35
|
+
// both sinks, so a record and an event cannot disagree about who produced them.
|
|
36
|
+
// `callPath` rides as an array rather than a joined string — the queryable form;
|
|
37
|
+
// rendering `parent > child` is the sink's choice, not a published package's.
|
|
38
|
+
const log = (opts.logger ?? createNoopLogger()).named("loop").with({ agentId: source.agentId, callPath: source.callPath });
|
|
28
39
|
const toolsById = new Map(agent.tools.map((t) => [t.id, t]));
|
|
29
40
|
const toolDefs = Object.fromEntries(agent.tools.map((t) => [
|
|
30
41
|
t.id,
|
|
@@ -40,6 +51,7 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
40
51
|
const unavailableAsk = new UnavailableAsk();
|
|
41
52
|
const ask = opts.ask ?? ((request) => unavailableAsk.ask(request));
|
|
42
53
|
const toolCtx = (tu) => ({
|
|
54
|
+
invocationId: tu.toolCallId,
|
|
43
55
|
session,
|
|
44
56
|
signal,
|
|
45
57
|
emit,
|
|
@@ -53,6 +65,12 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
53
65
|
// byte-identical to be read back.
|
|
54
66
|
const providerOptions = promptCacheProviderOptions(opts.promptCache ?? DEFAULT_PROMPT_CACHE);
|
|
55
67
|
const usage = {};
|
|
68
|
+
// Exactly one record per completed run — never one per iteration. That bound is
|
|
69
|
+
// what keeps the default level affordable for an agent that runs long; the
|
|
70
|
+
// per-iteration detail lives at `debug`, where paying per iteration is the point.
|
|
71
|
+
const logFinish = (level, reason, cappedOut) => {
|
|
72
|
+
log[level]("run finished", { iterations, reason, cappedOut, truncationRecoveries, usage });
|
|
73
|
+
};
|
|
56
74
|
// The user said no. A subsequent model call would only let the agent argue
|
|
57
75
|
// with the decision, or spend a call acknowledging it; the denial tool result
|
|
58
76
|
// is itself what the surface renders, so the turn ends the moment a denial
|
|
@@ -61,14 +79,21 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
61
79
|
const stopOnDenial = async (i) => {
|
|
62
80
|
await emit({ type: "iteration", source, index: i, final: true });
|
|
63
81
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: false, usage });
|
|
82
|
+
logFinish("warn", "denied", false);
|
|
64
83
|
return { messages, finish: { reason: "denied", cappedOut: false, truncationRecoveries } };
|
|
65
84
|
};
|
|
85
|
+
const stopOnResolved = async (i) => {
|
|
86
|
+
await emit({ type: "iteration", source, index: i, final: true });
|
|
87
|
+
recordAgentRun({ agentId: agent.id, iterations, cappedOut: false, usage });
|
|
88
|
+
return { messages, finish: { reason: "stop", cappedOut: false, truncationRecoveries } };
|
|
89
|
+
};
|
|
66
90
|
for (let i = 0; i < agent.maxIterations; i++) {
|
|
67
91
|
iterations = i + 1;
|
|
68
92
|
const request = {
|
|
69
93
|
system: agent.systemPrompt,
|
|
70
94
|
messages,
|
|
71
95
|
tools: toolDefs,
|
|
96
|
+
...(opts.toolChoice !== undefined ? { toolChoice: opts.toolChoice } : {}),
|
|
72
97
|
providerOptions,
|
|
73
98
|
};
|
|
74
99
|
const reply = await resultStep(runStep)(formatStepName.llm(i), () => provider.chat(request, session, signal));
|
|
@@ -92,6 +117,9 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
92
117
|
if (reply.finishReason === "length") {
|
|
93
118
|
truncationRecoveries++;
|
|
94
119
|
await emit({ type: "iteration", source, index: i, final: false });
|
|
120
|
+
// `tools` is what the model asked for; the trailing call was cut off at the
|
|
121
|
+
// output limit and is never dispatched, which the distinct message records.
|
|
122
|
+
log.debug("iteration truncated at output limit", { iteration: i, tools: toolCalls.map((t) => t.toolName), truncationRecoveries });
|
|
95
123
|
if (toolCalls.length === 0) {
|
|
96
124
|
// Stamped synthetic, not left as a bare `user` message: the wire format needs a user turn
|
|
97
125
|
// after a truncated assistant message, but this one is the loop's own nudge, and thread
|
|
@@ -120,14 +148,18 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
120
148
|
messages.push({ role: "tool", content: results });
|
|
121
149
|
if (hasDenial(results))
|
|
122
150
|
return stopOnDenial(i);
|
|
151
|
+
if (opts.resolved?.())
|
|
152
|
+
return stopOnResolved(i);
|
|
123
153
|
continue;
|
|
124
154
|
}
|
|
125
155
|
if (reply.finishReason !== "tool-calls") {
|
|
126
156
|
await emit({ type: "iteration", source, index: i, final: true });
|
|
127
157
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: false, usage });
|
|
158
|
+
logFinish("info", reply.finishReason, false);
|
|
128
159
|
return { messages, finish: { reason: reply.finishReason, cappedOut: false, truncationRecoveries } };
|
|
129
160
|
}
|
|
130
161
|
await emit({ type: "iteration", source, index: i, final: false });
|
|
162
|
+
log.debug("iteration", { iteration: i, tools: toolCalls.map((t) => t.toolName) });
|
|
131
163
|
for (const tu of toolCalls) {
|
|
132
164
|
await emit({ type: "tool-started", source, toolUseId: tu.toolCallId, name: tu.toolName, input: tu.input });
|
|
133
165
|
}
|
|
@@ -145,6 +177,8 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
145
177
|
messages.push({ role: "tool", content: results });
|
|
146
178
|
if (hasDenial(results))
|
|
147
179
|
return stopOnDenial(i);
|
|
180
|
+
if (opts.resolved?.())
|
|
181
|
+
return stopOnResolved(i);
|
|
148
182
|
}
|
|
149
183
|
// Cache defeater (known; not fixed here). Emptying the tool set changes the
|
|
150
184
|
// very front of the request prefix — tool definitions are cached ahead of
|
|
@@ -166,11 +200,13 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
166
200
|
markLastLoopAssistant(messages, initial.length);
|
|
167
201
|
await emit({ type: "iteration", source, index: agent.maxIterations, final: true });
|
|
168
202
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: true, usage });
|
|
203
|
+
logFinish("warn", "aborted", true);
|
|
169
204
|
return { messages, finish: { reason: "aborted", cappedOut: true, truncationRecoveries } };
|
|
170
205
|
}
|
|
171
206
|
messages.push(wrapUp.message);
|
|
172
207
|
await emit({ type: "iteration", source, index: agent.maxIterations, final: true });
|
|
173
208
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: true, usage });
|
|
209
|
+
logFinish("warn", "max_iterations", true);
|
|
174
210
|
return { messages, finish: { reason: "max_iterations", cappedOut: true, truncationRecoveries } };
|
|
175
211
|
}
|
|
176
212
|
function toolCallParts(message) {
|
|
@@ -256,8 +292,14 @@ async function execute(tu, tool, input, ctx, isFatalLoopError) {
|
|
|
256
292
|
return errorResult(tu, toolErrorContent(err));
|
|
257
293
|
}
|
|
258
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Normalize a tool's return value to plain JSON, stripping NUL from every string
|
|
297
|
+
* it holds ({@link stripNulCharacters}). The reviver sees values, not keys — a
|
|
298
|
+
* NUL in an object KEY would still reach the row, but keys come from tool result
|
|
299
|
+
* types rather than scanned bytes, so nothing produces one.
|
|
300
|
+
*/
|
|
259
301
|
function jsonValue(value) {
|
|
260
|
-
return JSON.parse(JSON.stringify(value ?? null));
|
|
302
|
+
return JSON.parse(JSON.stringify(value ?? null), (_key, v) => (typeof v === "string" ? stripNulCharacters(v) : v));
|
|
261
303
|
}
|
|
262
304
|
function successResult(toolCall, value) {
|
|
263
305
|
return {
|
|
@@ -267,12 +309,15 @@ function successResult(toolCall, value) {
|
|
|
267
309
|
output: { type: "json", value: jsonValue(value) },
|
|
268
310
|
};
|
|
269
311
|
}
|
|
312
|
+
// NUL is stripped from the prose BEFORE it is serialized: past the stringify it
|
|
313
|
+
// is the six-character escape \u0000 — legitimate JSON text, and no longer
|
|
314
|
+
// distinguishable from an error that happens to quote that escape.
|
|
270
315
|
function toolErrorContent(value) {
|
|
271
316
|
if (isToolError(value)) {
|
|
272
|
-
return JSON.stringify({ error: value.error, retryable: value.retryable });
|
|
317
|
+
return JSON.stringify({ error: stripNulCharacters(value.error), retryable: value.retryable });
|
|
273
318
|
}
|
|
274
319
|
const { retryable } = classifyProviderError(value);
|
|
275
|
-
const error = value instanceof Error ? value.message : String(value);
|
|
320
|
+
const error = stripNulCharacters(value instanceof Error ? value.message : String(value));
|
|
276
321
|
return JSON.stringify({ error, retryable });
|
|
277
322
|
}
|
|
278
323
|
function errorResult(toolCall, content) {
|
|
@@ -280,7 +325,8 @@ function errorResult(toolCall, content) {
|
|
|
280
325
|
type: "tool-result",
|
|
281
326
|
toolCallId: toolCall.toolCallId,
|
|
282
327
|
toolName: toolCall.toolName,
|
|
283
|
-
|
|
328
|
+
// A thrown tool error routinely quotes stderr verbatim — as exposed to NUL as a success payload.
|
|
329
|
+
output: { type: "error-text", value: stripNulCharacters(content) },
|
|
284
330
|
};
|
|
285
331
|
}
|
|
286
332
|
function isAskRejected(err) {
|
|
@@ -28,8 +28,6 @@ import type { AgentDefinition, LoopMessage } from "./types.js";
|
|
|
28
28
|
export declare const DEFAULT_SALVAGE_ITERATIONS = 3;
|
|
29
29
|
/** Describes how to salvage a run that never reached its terminal tool. */
|
|
30
30
|
export interface TerminalSalvage {
|
|
31
|
-
/** True once the agent recorded its terminal outcome (reads the closure cell). */
|
|
32
|
-
readonly resolved: () => boolean;
|
|
33
31
|
/** Terminal tools offered on the salvage turn (submit / blocker / …). Must be
|
|
34
32
|
* the same instances the first run used — they close over the outcome cell. */
|
|
35
33
|
readonly tools: readonly Tool[];
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* not collide the continuation's `llm-*` / `tool-*` cache keys with the first
|
|
21
21
|
* run's — replaying them would silently return the first run's cached results.
|
|
22
22
|
*/
|
|
23
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
23
24
|
import { DEFAULT_STEP_NAME_FORMATTER, runAgent } from "./run-agent.js";
|
|
24
25
|
/** Default salvage budget: one submit plus a validation-fix retry or two. */
|
|
25
26
|
export const DEFAULT_SALVAGE_ITERATIONS = 3;
|
|
@@ -36,8 +37,14 @@ function salvageStepNames(base) {
|
|
|
36
37
|
*/
|
|
37
38
|
export async function runToTerminal(agent, initial, session, opts, salvage) {
|
|
38
39
|
const first = await runAgent(agent, initial, session, opts);
|
|
39
|
-
if (
|
|
40
|
+
if (opts.resolved?.() || opts.signal.aborted)
|
|
40
41
|
return first;
|
|
42
|
+
// Reported here rather than in `runAgent` because the loop cannot know it is being
|
|
43
|
+
// salvaged: it sees an ordinary run with a small budget and a restricted tool set.
|
|
44
|
+
// Only this wrapper holds the fact that a first attempt ended without its outcome.
|
|
45
|
+
(opts.logger ?? createNoopLogger())
|
|
46
|
+
.named("loop")
|
|
47
|
+
.warn("salvaging a run that never reached its terminal tool", { agentId: agent.id, callPath: session.provenance.callPath });
|
|
41
48
|
const salvageAgent = {
|
|
42
49
|
...agent,
|
|
43
50
|
tools: [...salvage.tools],
|
|
@@ -20,8 +20,19 @@ export declare function parseStoredMessageEnvelope(value: unknown, identity: str
|
|
|
20
20
|
* harness fact from the loop, through `appendTurn`, into a row.
|
|
21
21
|
*/
|
|
22
22
|
export declare const HARNESS_PROVIDER_NAMESPACE = "cortex";
|
|
23
|
-
/** The {@link HARNESS_PROVIDER_NAMESPACE} key marking a message the loop synthesized rather than one a human sent. */
|
|
23
|
+
/** The {@link HARNESS_PROVIDER_NAMESPACE} key marking a message the loop or host synthesized rather than one a human sent. */
|
|
24
24
|
export declare const SYNTHETIC_MESSAGE_KEY = "synthetic";
|
|
25
|
+
/**
|
|
26
|
+
* The {@link HARNESS_PROVIDER_NAMESPACE} key marking a synthetic message that is a RECORD OF FACT
|
|
27
|
+
* the reader is meant to see, as opposed to loop machinery they are not.
|
|
28
|
+
*
|
|
29
|
+
* Always accompanies {@link SYNTHETIC_MESSAGE_KEY} — a record is synthetic in the sense that matters
|
|
30
|
+
* to every turn-boundary reader, and must never open a turn. This key answers the one question
|
|
31
|
+
* `synthetic` alone cannot: whether the message should be RENDERED. The loop's truncation nudge must
|
|
32
|
+
* not be (it would show words nobody typed); an embedder's out-of-band record must be (it is the
|
|
33
|
+
* only trace in the conversation that the work happened at all).
|
|
34
|
+
*/
|
|
35
|
+
export declare const SYNTHETIC_RECORD_KEY = "syntheticRecord";
|
|
25
36
|
/** The {@link HARNESS_PROVIDER_NAMESPACE} key marking an assistant message whose production a client abort cut off. */
|
|
26
37
|
export declare const INTERRUPTED_MESSAGE_KEY = "interrupted";
|
|
27
38
|
/**
|
|
@@ -39,18 +50,40 @@ export declare function markInterruptedMessage(message: ModelMessage): ModelMess
|
|
|
39
50
|
/** Whether a client abort cut off this message's production (see {@link markInterruptedMessage}); an absent key reads as not interrupted. */
|
|
40
51
|
export declare function isInterruptedMessage(message: ModelMessage): boolean;
|
|
41
52
|
/**
|
|
42
|
-
* Build a `user` message the
|
|
43
|
-
* truncated at its output-token limit.
|
|
53
|
+
* Build a `user` message the LOOP synthesized rather than a human typing it — today, the nudge that
|
|
54
|
+
* continues a reply the model truncated at its output-token limit.
|
|
44
55
|
*
|
|
45
56
|
* It has to carry the `user` role because the wire format requires a user turn after a truncated
|
|
46
57
|
* assistant message, but it is not user input, and the difference is load-bearing: a `user` message is
|
|
47
58
|
* what OPENS a conversation turn, so an unmarked synthetic one reads as a spurious turn boundary —
|
|
48
59
|
* splitting one turn into two for the token window and, worse, giving a tail-turn removal a cut point in
|
|
49
60
|
* the middle of a turn. Marking it is what lets {@link isSyntheticUserMessage} keep those readers honest.
|
|
61
|
+
*
|
|
62
|
+
* This one is loop MACHINERY and is not rendered: showing it would put words on screen nobody typed.
|
|
63
|
+
* An embedder recording out-of-band work — an analysis run's outcome — wants
|
|
64
|
+
* {@link syntheticRecordMessage} instead, which is equally non-turn-opening but IS displayed.
|
|
50
65
|
*/
|
|
51
66
|
export declare function syntheticUserMessage(text: string): ModelMessage;
|
|
52
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Build a record of out-of-band work for an embedder to append between turns — an analysis run's
|
|
69
|
+
* outcome, which the model should read on its next turn but which nobody said.
|
|
70
|
+
*
|
|
71
|
+
* Synthetic in every sense that matters to a turn-boundary reader ({@link isSyntheticUserMessage} is
|
|
72
|
+
* true of it), so it cannot split a turn for the token window nor hand a tail retraction a mid-turn
|
|
73
|
+
* cut point. It differs from {@link syntheticUserMessage} in exactly one respect, and it is the
|
|
74
|
+
* decisive one: this is a fact the reader is entitled to see, so the display reconstruction emits it
|
|
75
|
+
* — as a `system` message, which is what it is — rather than dropping it as loop machinery.
|
|
76
|
+
*
|
|
77
|
+
* A host MUST build these here rather than assembling the `providerOptions` markers itself: the keys
|
|
78
|
+
* and namespace are shared with the turn-boundary and display predicates, and a hand-rolled copy
|
|
79
|
+
* would fork them silently — the message would store fine and then be read as a genuine turn start,
|
|
80
|
+
* or be dropped from the transcript with no indication why.
|
|
81
|
+
*/
|
|
82
|
+
export declare function syntheticRecordMessage(text: string): ModelMessage;
|
|
83
|
+
/** Whether the loop or the host synthesized this message (see {@link syntheticUserMessage}) rather than a human sending it. */
|
|
53
84
|
export declare function isSyntheticUserMessage(message: ModelMessage): boolean;
|
|
85
|
+
/** Whether this is a host-appended record of out-of-band work (see {@link syntheticRecordMessage}) — synthetic, but displayed. */
|
|
86
|
+
export declare function isSyntheticRecordMessage(message: ModelMessage): boolean;
|
|
54
87
|
type LegacyContent = string | Array<Record<string, unknown>>;
|
|
55
88
|
export interface LegacyMessageRow {
|
|
56
89
|
readonly threadId: string;
|