@earendil-works/pi-durable 1.4.0 → 1.5.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/CHANGELOG.md +204 -0
- package/README.md +195 -85
- package/dist/codemode/codemode.d.ts +6 -5
- package/dist/codemode/codemode.d.ts.map +1 -1
- package/dist/codemode/codemode.js +10 -49
- package/dist/codemode/codemode.js.map +1 -1
- package/dist/codemode/models.d.ts +2 -1
- package/dist/codemode/models.d.ts.map +1 -1
- package/dist/codemode/models.js +4 -4
- package/dist/codemode/models.js.map +1 -1
- package/dist/documents.d.ts +1 -1
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +1 -1
- package/dist/documents.js.map +1 -1
- package/dist/entries.d.ts +1 -1
- package/dist/entries.d.ts.map +1 -1
- package/dist/entries.js.map +1 -1
- package/dist/env/index.d.ts +13 -11
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/node-watch.d.ts +4 -4
- package/dist/env/node-watch.d.ts.map +1 -1
- package/dist/env/node-watch.js +18 -16
- package/dist/env/node-watch.js.map +1 -1
- package/dist/env/node.d.ts +3 -3
- package/dist/env/node.d.ts.map +1 -1
- package/dist/env/node.js +1 -1
- package/dist/env/node.js.map +1 -1
- package/dist/errors.d.ts +18 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +26 -3
- package/dist/errors.js.map +1 -1
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +37 -3
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/compaction.d.ts +8 -3
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +40 -11
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/context.d.ts +24 -8
- package/dist/harness/context.d.ts.map +1 -1
- package/dist/harness/context.js +71 -25
- package/dist/harness/context.js.map +1 -1
- package/dist/harness/define.d.ts +8 -4
- package/dist/harness/define.d.ts.map +1 -1
- package/dist/harness/define.js +4 -0
- package/dist/harness/define.js.map +1 -1
- package/dist/harness/generation.d.ts +39 -6
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +451 -100
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts +3 -1
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +87 -62
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/inbox.d.ts +7 -2
- package/dist/harness/inbox.d.ts.map +1 -1
- package/dist/harness/inbox.js +5 -2
- package/dist/harness/inbox.js.map +1 -1
- package/dist/harness/json.d.ts +21 -5
- package/dist/harness/json.d.ts.map +1 -1
- package/dist/harness/json.js +23 -5
- package/dist/harness/json.js.map +1 -1
- package/dist/harness/live.d.ts +124 -17
- package/dist/harness/live.d.ts.map +1 -1
- package/dist/harness/live.js +93 -12
- package/dist/harness/live.js.map +1 -1
- package/dist/harness/partial-changes.d.ts +54 -0
- package/dist/harness/partial-changes.d.ts.map +1 -0
- package/dist/harness/partial-changes.js +163 -0
- package/dist/harness/partial-changes.js.map +1 -0
- package/dist/harness/prompt.d.ts +1 -1
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/provider.d.ts +1 -1
- package/dist/harness/provider.d.ts.map +1 -1
- package/dist/harness/provider.js.map +1 -1
- package/dist/harness/registry-reader.d.ts +11 -0
- package/dist/harness/registry-reader.d.ts.map +1 -0
- package/dist/harness/registry-reader.js +25 -0
- package/dist/harness/registry-reader.js.map +1 -0
- package/dist/harness/scheduler.d.ts +48 -20
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +1006 -380
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +18 -11
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +63 -26
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/task-graph.d.ts +14 -9
- package/dist/harness/task-graph.d.ts.map +1 -1
- package/dist/harness/task-graph.js +38 -28
- package/dist/harness/task-graph.js.map +1 -1
- package/dist/harness/telemetry.d.ts +11 -11
- package/dist/harness/telemetry.d.ts.map +1 -1
- package/dist/harness/telemetry.js +7 -7
- package/dist/harness/telemetry.js.map +1 -1
- package/dist/harness/tool.d.ts +3 -1
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +172 -56
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/transcript.d.ts +72 -0
- package/dist/harness/transcript.d.ts.map +1 -0
- package/dist/harness/transcript.js +264 -0
- package/dist/harness/transcript.js.map +1 -0
- package/dist/harness/types.d.ts +237 -61
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/usage.d.ts +2 -1
- package/dist/harness/usage.d.ts.map +1 -1
- package/dist/harness/usage.js +1 -1
- package/dist/harness/usage.js.map +1 -1
- package/dist/harness/util.d.ts +4 -2
- package/dist/harness/util.d.ts.map +1 -1
- package/dist/harness/util.js +9 -0
- package/dist/harness/util.js.map +1 -1
- package/dist/harness/view.d.ts +44 -22
- package/dist/harness/view.d.ts.map +1 -1
- package/dist/harness/view.js +130 -71
- package/dist/harness/view.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/search.d.ts +19 -3
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +107 -44
- package/dist/search.js.map +1 -1
- package/dist/session/cached-storage.d.ts +72 -0
- package/dist/session/cached-storage.d.ts.map +1 -0
- package/dist/session/cached-storage.js +269 -0
- package/dist/session/cached-storage.js.map +1 -0
- package/dist/session/conversation-tails.d.ts +33 -0
- package/dist/session/conversation-tails.d.ts.map +1 -0
- package/dist/session/conversation-tails.js +101 -0
- package/dist/session/conversation-tails.js.map +1 -0
- package/dist/session/forks.d.ts +1 -1
- package/dist/session/forks.d.ts.map +1 -1
- package/dist/session/forks.js.map +1 -1
- package/dist/session/keys.d.ts +69 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +161 -0
- package/dist/session/keys.js.map +1 -0
- package/dist/session/session.d.ts +169 -26
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +1029 -171
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts +84 -5
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +227 -40
- package/dist/session/transaction.js.map +1 -1
- package/dist/session/writer.d.ts +74 -0
- package/dist/session/writer.d.ts.map +1 -0
- package/dist/session/writer.js +294 -0
- package/dist/session/writer.js.map +1 -0
- package/dist/storage/indexeddb.d.ts +12 -3
- package/dist/storage/indexeddb.d.ts.map +1 -1
- package/dist/storage/indexeddb.js +91 -41
- package/dist/storage/indexeddb.js.map +1 -1
- package/dist/storage/jsonl/index.d.ts +1 -1
- package/dist/storage/jsonl/index.d.ts.map +1 -1
- package/dist/storage/jsonl/index.js +1 -1
- package/dist/storage/jsonl/index.js.map +1 -1
- package/dist/storage/jsonl/node.d.ts +3 -3
- package/dist/storage/jsonl/node.d.ts.map +1 -1
- package/dist/storage/jsonl/node.js +2 -2
- package/dist/storage/jsonl/node.js.map +1 -1
- package/dist/storage/jsonl/storage.d.ts +46 -5
- package/dist/storage/jsonl/storage.d.ts.map +1 -1
- package/dist/storage/jsonl/storage.js +158 -43
- package/dist/storage/jsonl/storage.js.map +1 -1
- package/dist/storage/memory.d.ts +9 -3
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +28 -2
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite/commit.d.ts +52 -0
- package/dist/storage/sqlite/commit.d.ts.map +1 -0
- package/dist/storage/sqlite/commit.js +371 -0
- package/dist/storage/sqlite/commit.js.map +1 -0
- package/dist/storage/sqlite/rows.d.ts +33 -0
- package/dist/storage/sqlite/rows.d.ts.map +1 -0
- package/dist/storage/sqlite/rows.js +53 -0
- package/dist/storage/sqlite/rows.js.map +1 -0
- package/dist/storage/sqlite/statements.d.ts +13 -0
- package/dist/storage/sqlite/statements.d.ts.map +1 -0
- package/dist/storage/sqlite/statements.js +88 -0
- package/dist/storage/sqlite/statements.js.map +1 -0
- package/dist/storage/sqlite/storage.d.ts +23 -10
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +120 -261
- package/dist/storage/sqlite/storage.js.map +1 -1
- package/dist/storage/store/commit.d.ts +33 -0
- package/dist/storage/store/commit.d.ts.map +1 -0
- package/dist/storage/store/commit.js +286 -0
- package/dist/storage/store/commit.js.map +1 -0
- package/dist/storage/store/documents.d.ts +43 -0
- package/dist/storage/store/documents.d.ts.map +1 -0
- package/dist/storage/store/documents.js +102 -0
- package/dist/storage/store/documents.js.map +1 -0
- package/dist/storage/store/index.d.ts +2 -0
- package/dist/storage/store/index.d.ts.map +1 -0
- package/dist/storage/store/index.js +2 -0
- package/dist/storage/store/index.js.map +1 -0
- package/dist/storage/store/queries.d.ts +49 -0
- package/dist/storage/store/queries.d.ts.map +1 -0
- package/dist/storage/store/queries.js +93 -0
- package/dist/storage/store/queries.js.map +1 -0
- package/dist/storage/store/storage.d.ts +60 -0
- package/dist/storage/store/storage.d.ts.map +1 -0
- package/dist/storage/store/storage.js +438 -0
- package/dist/storage/store/storage.js.map +1 -0
- package/dist/storage/store/tables.d.ts +54 -0
- package/dist/storage/store/tables.d.ts.map +1 -0
- package/dist/storage/store/tables.js +108 -0
- package/dist/storage/store/tables.js.map +1 -0
- package/dist/testing/commit-writes.d.ts +5 -0
- package/dist/testing/commit-writes.d.ts.map +1 -0
- package/dist/testing/commit-writes.js +12 -0
- package/dist/testing/commit-writes.js.map +1 -0
- package/dist/testing/env-conformance.js +7 -7
- package/dist/testing/env-conformance.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/storage-benchmark.d.ts.map +1 -1
- package/dist/testing/storage-benchmark.js +18 -17
- package/dist/testing/storage-benchmark.js.map +1 -1
- package/dist/testing/storage-conformance.d.ts.map +1 -1
- package/dist/testing/storage-conformance.js +354 -69
- package/dist/testing/storage-conformance.js.map +1 -1
- package/dist/tool-search/tool-search.d.ts +8 -14
- package/dist/tool-search/tool-search.d.ts.map +1 -1
- package/dist/tool-search/tool-search.js +25 -13
- package/dist/tool-search/tool-search.js.map +1 -1
- package/dist/tools/bash.d.ts +13 -13
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +16 -15
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +7 -7
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +9 -9
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/file-mutation-queue.d.ts +1 -1
- package/dist/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/tools/file-mutation-queue.js.map +1 -1
- package/dist/tools/index.d.ts +32 -32
- package/dist/tools/path-utils.d.ts +1 -1
- package/dist/tools/path-utils.d.ts.map +1 -1
- package/dist/tools/path-utils.js.map +1 -1
- package/dist/tools/read.d.ts +6 -6
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +19 -8
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +4 -4
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +5 -5
- package/dist/tools/write.js.map +1 -1
- package/dist/types.d.ts +228 -78
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +19 -12
- package/dist/harness/events.d.ts +0 -186
- package/dist/harness/events.d.ts.map +0 -1
- package/dist/harness/events.js +0 -373
- package/dist/harness/events.js.map +0 -1
- package/dist/session/observation.d.ts +0 -42
- package/dist/session/observation.d.ts.map +0 -1
- package/dist/session/observation.js +0 -281
- package/dist/session/observation.js.map +0 -1
package/dist/harness/tool.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { copyJson } from "@earendil-works/chord";
|
|
2
1
|
import { awaitWithContext } from "@earendil-works/chord/context";
|
|
3
|
-
import { overlap } from "@earendil-works/chord/delta";
|
|
2
|
+
import { overlap, ownedSlice } from "@earendil-works/chord/delta";
|
|
3
|
+
import { copyJson } from "@earendil-works/chord/json";
|
|
4
4
|
import { boundOutput, OutputBuffer, PROGRESS_BYTES_PER_SECOND, Progress, } from "@earendil-works/chord/output";
|
|
5
5
|
import { validateToolArguments } from "@earendil-works/pi-ai/utils/validation";
|
|
6
|
-
import {
|
|
6
|
+
import { compile } from "@earendil-works/pi-schema";
|
|
7
|
+
import { formats } from "@earendil-works/pi-schema/formats";
|
|
7
8
|
import { defineDocFamily } from "../documents.js";
|
|
8
9
|
import { AssistantEntry, ToolResultEntry } from "../entries.js";
|
|
9
10
|
import { errorMessage } from "../errors.js";
|
|
@@ -11,7 +12,7 @@ import { defineTask } from "../tasks.js";
|
|
|
11
12
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, utf8ByteLength } from "../truncate.js";
|
|
12
13
|
import { toolExtension } from "./agent.js";
|
|
13
14
|
import { assignJson } from "./json.js";
|
|
14
|
-
import { clearProgress, finishSlot, LiveDoc, removeNestedSlots, toolSlot, } from "./live.js";
|
|
15
|
+
import { clearProgress, cutOffCount, cutOffTooOften, finishSlot, LiveDoc, PROGRESS_COMMIT, removeNestedSlots, salvageable, toolSlot, } from "./live.js";
|
|
15
16
|
import { recordUsage } from "./usage.js";
|
|
16
17
|
import { optionalExtension } from "./util.js";
|
|
17
18
|
/**
|
|
@@ -41,25 +42,29 @@ export const NestedCallDoc = defineDocFamily({
|
|
|
41
42
|
*/
|
|
42
43
|
export const ToolTask = defineTask({
|
|
43
44
|
name: "pi.tool",
|
|
44
|
-
version:
|
|
45
|
+
version: 3,
|
|
45
46
|
initial: () => ({ phase: "call" }),
|
|
46
|
-
// Version 1 had only model-issued calls.
|
|
47
|
-
migrate: (input, checkpoint) => ({
|
|
48
|
-
input:
|
|
47
|
+
// Version 1 had only model-issued calls, without `kind`; version 3 added `unoffered`, absent in version 2 inputs.
|
|
48
|
+
migrate: (input, checkpoint, fromVersion) => ({
|
|
49
|
+
input: fromVersion === 1
|
|
50
|
+
? { kind: "model", ...input }
|
|
51
|
+
: input,
|
|
49
52
|
checkpoint: checkpoint,
|
|
50
53
|
}),
|
|
51
54
|
phases: {
|
|
52
55
|
call: async (task, runtime, context) => {
|
|
53
56
|
const call = await readCall(runtime, task.input, context);
|
|
54
|
-
|
|
57
|
+
if (cutOffTooOften(task, runtime))
|
|
58
|
+
return endCutOff(runtime, task, call, context);
|
|
59
|
+
const tool = isUnoffered(task.input) ? undefined : await resolveTool(runtime, task.input, call.name, context);
|
|
55
60
|
if (tool === undefined) {
|
|
56
61
|
const error = harnessError("tool_unavailable", `Tool ${call.name} is not available`);
|
|
57
|
-
return
|
|
62
|
+
return reject(runtime, task.input, call, undefined, error, context);
|
|
58
63
|
}
|
|
59
64
|
const prepared = prepare(tool, call.arguments);
|
|
60
65
|
const checked = "error" in prepared ? prepared : validate(tool, call, prepared.args);
|
|
61
66
|
if ("error" in checked)
|
|
62
|
-
return
|
|
67
|
+
return reject(runtime, task.input, call, tool, invalid(checked.error), context);
|
|
63
68
|
let args = checked.args;
|
|
64
69
|
let block;
|
|
65
70
|
await runtime.hooks.each("beforeTool", async (hook) => {
|
|
@@ -80,22 +85,26 @@ export const ToolTask = defineTask({
|
|
|
80
85
|
});
|
|
81
86
|
if (block !== undefined) {
|
|
82
87
|
const blocked = harnessError("blocked", `Tool call blocked: ${block}`);
|
|
83
|
-
return
|
|
88
|
+
return reject(runtime, task.input, call, tool, blocked, context);
|
|
84
89
|
}
|
|
85
90
|
const validated = validate(tool, call, args);
|
|
86
91
|
if ("error" in validated)
|
|
87
|
-
return
|
|
92
|
+
return reject(runtime, task.input, call, tool, invalid(validated.error), context);
|
|
88
93
|
// Strict JSON: repair may set optional properties to undefined, which the slot and checkpoint cannot hold.
|
|
89
94
|
const final = copyJson(validated.args, { omitUndefinedProperties: true });
|
|
95
|
+
// The slot's status and arguments are presentation, stored like progress (`PROGRESS_COMMIT`); the intent is
|
|
96
|
+
// durable before the tool runs. Parallel calls of one round then share no immediate write to `pi.live`.
|
|
90
97
|
await runtime.commit(async (tx) => {
|
|
91
98
|
const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId);
|
|
92
99
|
if (slot !== undefined)
|
|
93
100
|
slot.status = "running";
|
|
94
101
|
// A nested slot shows the arguments the call runs with, which repair, hooks, and coercion may have changed.
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
if (changed && slot !== undefined && "parentTaskId" in slot)
|
|
102
|
+
// Assigned whole: an equal value records no change, and a rerun corrects what an earlier attempt showed.
|
|
103
|
+
if (slot !== undefined && "parentTaskId" in slot)
|
|
98
104
|
slot.arguments = final;
|
|
105
|
+
return undefined;
|
|
106
|
+
}, context, PROGRESS_COMMIT);
|
|
107
|
+
await runtime.commit(() => {
|
|
99
108
|
const intent = { phase: "execute", arguments: final, replay: tool.replay ?? "unsafe" };
|
|
100
109
|
return { status: "running", checkpoint: intent };
|
|
101
110
|
}, context);
|
|
@@ -105,13 +114,21 @@ export const ToolTask = defineTask({
|
|
|
105
114
|
execute: async (task, runtime, context) => {
|
|
106
115
|
const { arguments: args, replay } = task.state.checkpoint;
|
|
107
116
|
const call = await readCall(runtime, task.input, context);
|
|
117
|
+
// Also before an interruption result: its `afterTool` hooks may be what cut the task off.
|
|
118
|
+
if (cutOffTooOften(task, runtime))
|
|
119
|
+
return endCutOff(runtime, task, call, context);
|
|
108
120
|
const tool = await resolveTool(runtime, task.input, call.name, context);
|
|
109
121
|
if (replay === "safe" && tool?.replay === "safe") {
|
|
110
|
-
// The rerun reports from scratch
|
|
122
|
+
// The rerun reports from scratch: clear what the interrupted attempt published, and show the slot running with
|
|
123
|
+
// the arguments it runs with again, as the deferred commit that showed them may have been lost with the crash.
|
|
111
124
|
await runtime.commit(async (tx) => {
|
|
112
125
|
const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId);
|
|
113
|
-
if (slot !== undefined)
|
|
126
|
+
if (slot !== undefined) {
|
|
114
127
|
clearProgress(slot);
|
|
128
|
+
slot.status = "running";
|
|
129
|
+
if ("parentTaskId" in slot)
|
|
130
|
+
slot.arguments = args;
|
|
131
|
+
}
|
|
115
132
|
return undefined;
|
|
116
133
|
}, context);
|
|
117
134
|
return run(runtime, task.input, call, tool, args, context);
|
|
@@ -119,26 +136,92 @@ export const ToolTask = defineTask({
|
|
|
119
136
|
const message = `Tool ${call.name} was interrupted and may have partially run`;
|
|
120
137
|
// `failed` records cancellation intent, so the call's owned conversations, left unsupervised, are aborted.
|
|
121
138
|
const ending = { status: "failed", message };
|
|
122
|
-
|
|
139
|
+
// Not cut off too often (checked above), so the hooks run.
|
|
140
|
+
await settleFromSlot(runtime, task.input, call, { tool }, ending, "interrupted", message, context);
|
|
123
141
|
},
|
|
124
142
|
},
|
|
125
143
|
abort: async (task, runtime, context) => {
|
|
126
144
|
const call = await readCall(runtime, task.input, context);
|
|
127
|
-
// Abandoned after a restart (`TaskOptions.abandonOnRestart`): say whether the call may have run.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
// Abandoned after a restart (`TaskOptions.abandonOnRestart`): say whether the call may have run. An unoffered call
|
|
146
|
+
// could not run in any case, and its result says so.
|
|
147
|
+
const [code, message] = isUnoffered(task.input)
|
|
148
|
+
? ["tool_unavailable", `Tool ${call.name} is not available`]
|
|
149
|
+
: task.abortReason !== "restart"
|
|
150
|
+
? ["aborted", `Tool ${call.name} was aborted`]
|
|
151
|
+
: task.state.checkpoint.phase === "execute"
|
|
152
|
+
? ["interrupted", `Tool ${call.name} was interrupted by a restart and may have partially run`]
|
|
153
|
+
: ["abandoned", `Tool ${call.name} was not started: its caller ended with a restart`];
|
|
154
|
+
// The scheduler runs an abort handler only while its restarts are below the limit, and faults the task without a
|
|
155
|
+
// result when the last attempt is cut off. That attempt skips the hooks, which may be what cuts it off.
|
|
156
|
+
const hooks = (task.restarts ?? 0) < runtime.settings.restartLimit - 1
|
|
157
|
+
? { tool: await toolForLimits(runtime, task.input, call.name, context) }
|
|
158
|
+
: undefined;
|
|
159
|
+
await settleFromSlot(runtime, task.input, call, hooks, { status: "aborted" }, code, message, context);
|
|
135
160
|
},
|
|
136
161
|
});
|
|
162
|
+
/**
|
|
163
|
+
* End a call cut off `settings.restartLimit` times in a row (spec §5.4) with an error result, built from the slot like
|
|
164
|
+
* an interrupted call's, instead of running it again. `failed`, like an interruption, so its owned work is aborted.
|
|
165
|
+
*/
|
|
166
|
+
async function endCutOff(runtime, task, call, context) {
|
|
167
|
+
const message = `Tool ${call.name} was interrupted ${cutOffCount(task)}; it is not run again`;
|
|
168
|
+
const ending = { status: "failed", message, reason: "restart_limit" };
|
|
169
|
+
// Past the limit the hooks may be what keeps cutting the task off (`salvageable`).
|
|
170
|
+
const hooks = salvageable(task, runtime)
|
|
171
|
+
? { tool: await toolForLimits(runtime, task.input, call.name, context) }
|
|
172
|
+
: undefined;
|
|
173
|
+
await settleFromSlot(runtime, task.input, call, hooks, ending, "restart_limit", message, context);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Settle a result the Harness decided in the `call` phase, without running the tool: the tool did not resolve, the
|
|
177
|
+
* arguments are invalid, or `beforeTool` blocked the call. `afterTool` applies as to an executed call's result.
|
|
178
|
+
*/
|
|
179
|
+
async function reject(runtime, input, call, tool, result, context) {
|
|
180
|
+
const settled = await afterHarnessResult(runtime, input, call, tool, result, undefined, context);
|
|
181
|
+
await settle(runtime, input, call, COMPLETED, settled, context);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Settle an error result built from the call's slot (`fromSlot`): an interruption, the restart limit, or an abort.
|
|
185
|
+
* The tool is not running, so the slot read before the commit is still current when it commits. With `hooks`, the
|
|
186
|
+
* result passes `afterTool` and is bounded with the limits of `hooks.tool` (the defaults when it is `undefined`);
|
|
187
|
+
* without, it is settled as built, when the hooks may be what cuts the task off.
|
|
188
|
+
*/
|
|
189
|
+
async function settleFromSlot(runtime, input, call, hooks, ending, code, message, context) {
|
|
190
|
+
const live = await runtime.snapshot(LiveDoc, runtime.conversationId, context);
|
|
191
|
+
const { result, truncation } = fromSlot(live === undefined ? undefined : toolSlot(live, runtime.taskId), code, message);
|
|
192
|
+
const settled = hooks !== undefined
|
|
193
|
+
? await afterHarnessResult(runtime, input, call, hooks.tool, result, truncation, context)
|
|
194
|
+
: truncation === undefined
|
|
195
|
+
? result
|
|
196
|
+
: { ...result, diagnostics: [...(result.diagnostics ?? []), truncation] };
|
|
197
|
+
await settle(runtime, input, call, ending, settled, context);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The tool whose output limits bound a hooks' result: the called tool, or none when it, or the agent, fails to resolve,
|
|
201
|
+
* and for an unoffered call, which never resolves its name.
|
|
202
|
+
*/
|
|
203
|
+
async function toolForLimits(runtime, input, name, context) {
|
|
204
|
+
if (isUnoffered(input))
|
|
205
|
+
return undefined;
|
|
206
|
+
return resolveTool(runtime, input, name, context).catch(() => undefined);
|
|
207
|
+
}
|
|
208
|
+
/** A model-issued call to a tool its request did not offer (spec §8.3). */
|
|
209
|
+
function isUnoffered(input) {
|
|
210
|
+
return input.kind === "model" && input.unoffered === true;
|
|
211
|
+
}
|
|
137
212
|
/** The called tool: among the tools the model is offered for a model-issued call, else among those tools may call. */
|
|
138
213
|
async function resolveTool(runtime, input, name, context) {
|
|
139
214
|
const agent = await runtime.agent(context);
|
|
140
215
|
return (input.kind === "model" ? agent.tools : agent.callable).find((tool) => tool.name === name);
|
|
141
216
|
}
|
|
217
|
+
/** The tool's output limits, the defaults filled in; all defaults when the tool did not resolve. */
|
|
218
|
+
function outputLimits(tool) {
|
|
219
|
+
return {
|
|
220
|
+
maxBytes: tool?.outputLimits?.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
221
|
+
maxLines: tool?.outputLimits?.maxLines ?? DEFAULT_MAX_LINES,
|
|
222
|
+
retain: tool?.outputLimits?.retain ?? "head",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
142
225
|
/** The call: from the input for a nested call, which hooks see with its parent, else from the assistant entry. */
|
|
143
226
|
async function readCall(runtime, input, context) {
|
|
144
227
|
if (input.kind === "nested") {
|
|
@@ -186,11 +269,7 @@ function invalid(message) {
|
|
|
186
269
|
}
|
|
187
270
|
/** Execute with the resolved implementation, then settle its result. */
|
|
188
271
|
async function run(runtime, input, call, tool, args, context) {
|
|
189
|
-
const limits =
|
|
190
|
-
maxBytes: tool.outputLimits?.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
191
|
-
maxLines: tool.outputLimits?.maxLines ?? DEFAULT_MAX_LINES,
|
|
192
|
-
retain: tool.outputLimits?.retain ?? "head",
|
|
193
|
-
};
|
|
272
|
+
const limits = outputLimits(tool);
|
|
194
273
|
const reported = { output: new OutputBuffer(limits), limits, diagnostics: [], details: undefined };
|
|
195
274
|
const streams = input.kind === "model" || input.progress !== false;
|
|
196
275
|
const progress = publishProgress(runtime, reported, streams, context);
|
|
@@ -255,12 +334,12 @@ async function run(runtime, input, call, tool, args, context) {
|
|
|
255
334
|
committed.catch(() => { });
|
|
256
335
|
return awaitWithContext(committed, detailsContext);
|
|
257
336
|
},
|
|
258
|
-
commit: async (change, commitContext) => {
|
|
337
|
+
commit: async (change, commitContext, options) => {
|
|
259
338
|
let result;
|
|
260
339
|
await runtime.commit(async (tx) => {
|
|
261
340
|
result = await change(tx);
|
|
262
341
|
return undefined;
|
|
263
|
-
}, commitContext);
|
|
342
|
+
}, commitContext, options);
|
|
264
343
|
return result;
|
|
265
344
|
},
|
|
266
345
|
memo: runtime.memo,
|
|
@@ -335,7 +414,7 @@ async function run(runtime, input, call, tool, args, context) {
|
|
|
335
414
|
const pending = await progress.stop();
|
|
336
415
|
try {
|
|
337
416
|
const settled = await finalResult(runtime, input, call, tool, result, reported, context);
|
|
338
|
-
await settle(runtime, input, call, ending,
|
|
417
|
+
await settle(runtime, input, call, ending, settled, context, durationMs);
|
|
339
418
|
}
|
|
340
419
|
catch (error) {
|
|
341
420
|
for (const waiter of pending)
|
|
@@ -366,7 +445,7 @@ function progressClock(runtime) {
|
|
|
366
445
|
}
|
|
367
446
|
/**
|
|
368
447
|
* Throttled commits of what the tool reported into its slot, in `pi.live.tools` or `pi.live.nestedTools`, each writing
|
|
369
|
-
* only what changed since the last one; none when `enabled` is false.
|
|
448
|
+
* only what changed since the last one, deferred (`PROGRESS_COMMIT`); none when `enabled` is false.
|
|
370
449
|
*/
|
|
371
450
|
function publishProgress(runtime, reported, enabled, context) {
|
|
372
451
|
let written = { text: "", details: undefined, diagnostics: 0 };
|
|
@@ -414,7 +493,7 @@ function publishProgress(runtime, reported, enabled, context) {
|
|
|
414
493
|
slot.diagnostics.push(diagnostic);
|
|
415
494
|
}
|
|
416
495
|
return undefined;
|
|
417
|
-
}, context);
|
|
496
|
+
}, context, PROGRESS_COMMIT);
|
|
418
497
|
written = current;
|
|
419
498
|
return bytes;
|
|
420
499
|
}, (error) => {
|
|
@@ -461,7 +540,7 @@ async function finalResult(runtime, input, call, tool, result, reported, context
|
|
|
461
540
|
final = rest;
|
|
462
541
|
// Only names the extension: a failed resolution leaves the report without the name.
|
|
463
542
|
const extension = await runtime.agent(context).then((agent) => toolExtension(agent, tool.name), () => undefined);
|
|
464
|
-
runtime.report(new Error(broken), {
|
|
543
|
+
runtime.report(new Error(broken), { kind: "extension", ...optionalExtension(extension) });
|
|
465
544
|
}
|
|
466
545
|
}
|
|
467
546
|
// Programs get the tool's own value: without a schema, its output. The model reads only `output`, so model-issued
|
|
@@ -482,6 +561,33 @@ async function finalResult(runtime, input, call, tool, result, reported, context
|
|
|
482
561
|
}
|
|
483
562
|
return { ...final, output: bounded.content, diagnostics: [...(final.diagnostics ?? []), ...harness] };
|
|
484
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* A result the Harness wrote itself, without executing the tool, after the `afterTool` chain, normalized like an
|
|
566
|
+
* executed call's: the output bounded with the tool's limits (the defaults when it did not resolve), a truncation
|
|
567
|
+
* diagnostic when that dropped something, and the hooks' `structuredOutput` dropped, since the tool never produced
|
|
568
|
+
* one. A nested call that the hooks turned into a success gets the bounded output as its value, as a schema-less
|
|
569
|
+
* tool's success does, unless the tool declares a schema. `usage` is kept and recorded; `control` applies only when
|
|
570
|
+
* the task completes. `truncation` describes what the result's own output lost before (a slot's retained output); it
|
|
571
|
+
* is kept only while the hooks keep that output.
|
|
572
|
+
*/
|
|
573
|
+
async function afterHarnessResult(runtime, input, call, tool, result, truncation, context) {
|
|
574
|
+
let final = result;
|
|
575
|
+
await runtime.hooks.each("afterTool", async (hook) => {
|
|
576
|
+
final = (await hook(call, final, runtime, context)) ?? final;
|
|
577
|
+
});
|
|
578
|
+
const harness = [];
|
|
579
|
+
if (truncation !== undefined && final.output === result.output)
|
|
580
|
+
harness.push(truncation);
|
|
581
|
+
const limits = outputLimits(tool);
|
|
582
|
+
const bounded = boundContent(final.output ?? [], limits);
|
|
583
|
+
if (bounded.droppedBytes > 0)
|
|
584
|
+
harness.push(truncated(bounded, limits.retain));
|
|
585
|
+
const { structuredOutput: _dropped, ...rest } = final;
|
|
586
|
+
const value = input.kind === "nested" && final.isError !== true && tool?.structuredOutputSchema === undefined
|
|
587
|
+
? { structuredOutput: outputValue(bounded.content) }
|
|
588
|
+
: {};
|
|
589
|
+
return { ...rest, ...value, output: bounded.content, diagnostics: [...(rest.diagnostics ?? []), ...harness] };
|
|
590
|
+
}
|
|
485
591
|
const structuredValidators = new WeakMap();
|
|
486
592
|
/** Why a result's `structuredOutput` breaks the tool's contract, or `undefined` when it keeps it. */
|
|
487
593
|
function structuredOutputError(tool, result) {
|
|
@@ -497,23 +603,26 @@ function structuredOutputError(tool, result) {
|
|
|
497
603
|
}
|
|
498
604
|
let validator = structuredValidators.get(schema);
|
|
499
605
|
if (validator === undefined) {
|
|
500
|
-
validator =
|
|
606
|
+
validator = compile(schema, { formats, unresolvedRefs: "reject" });
|
|
501
607
|
structuredValidators.set(schema, validator);
|
|
502
608
|
}
|
|
503
|
-
if (validator.
|
|
609
|
+
if (validator.check(value))
|
|
504
610
|
return undefined;
|
|
505
|
-
const [first] = validator.
|
|
506
|
-
const path = first?.
|
|
611
|
+
const [first] = validator.errors(value);
|
|
612
|
+
const path = first?.path
|
|
613
|
+
.slice(1)
|
|
614
|
+
.split("/")
|
|
615
|
+
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"))
|
|
616
|
+
.join(".") || "root";
|
|
507
617
|
return `Tool ${tool.name} returned structuredOutput that does not match its schema: ${path}: ${first?.message ?? "invalid"}`;
|
|
508
618
|
}
|
|
509
619
|
/**
|
|
510
620
|
* Commit the tool's terminal state and mark its slot done. A model-issued call appends its result entry and ends with
|
|
511
621
|
* the entry ID; a nested call writes its result to the caller's `NestedCallDoc`, records its usage, and ends with a
|
|
512
622
|
* small receipt. Nested calls this call left running are aborted first, so they report into their slots before the
|
|
513
|
-
* slots below the call leave `pi.live`. `
|
|
514
|
-
* partial output.
|
|
623
|
+
* slots below the call leave `pi.live`. `control` applies only when the task completes.
|
|
515
624
|
*/
|
|
516
|
-
async function settle(runtime, input, call, ending,
|
|
625
|
+
async function settle(runtime, input, call, ending, result, context, durationMs) {
|
|
517
626
|
// A crash in between leaves the call in `execute`: a safe rerun reattaches to the calls, an interruption ends here.
|
|
518
627
|
// Every live nested call the task owns, also those an earlier attempt made and this one never called again.
|
|
519
628
|
const nestedIds = (await runtime.ownedTasks(context))
|
|
@@ -527,7 +636,6 @@ async function settle(runtime, input, call, ending, build, context, durationMs)
|
|
|
527
636
|
const conversationId = runtime.conversationId;
|
|
528
637
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
529
638
|
const slot = toolSlot(live, runtime.taskId);
|
|
530
|
-
const result = build(slot);
|
|
531
639
|
let settled;
|
|
532
640
|
if (input.kind === "nested") {
|
|
533
641
|
const nested = nestedResult(runtime.taskId, result, durationMs);
|
|
@@ -565,7 +673,10 @@ async function settle(runtime, input, call, ending, build, context, durationMs)
|
|
|
565
673
|
if (ending.status === "aborted")
|
|
566
674
|
return { status: "terminal", outcome: { status: "aborted", result: settled } };
|
|
567
675
|
if (ending.status === "failed") {
|
|
568
|
-
const error = {
|
|
676
|
+
const error = {
|
|
677
|
+
message: ending.message,
|
|
678
|
+
...(ending.reason === undefined ? {} : { detail: { reason: ending.reason } }),
|
|
679
|
+
};
|
|
569
680
|
return { status: "terminal", outcome: { status: "failed", error, result: settled } };
|
|
570
681
|
}
|
|
571
682
|
return { status: "terminal", outcome: { status: "completed", result: settled } };
|
|
@@ -674,19 +785,21 @@ function fallbackResult(taskId, name, outcome) {
|
|
|
674
785
|
return { taskId, isError: true, diagnostics: error.diagnostics ?? [] };
|
|
675
786
|
}
|
|
676
787
|
const COMPLETED = { status: "completed" };
|
|
677
|
-
/**
|
|
788
|
+
/**
|
|
789
|
+
* An error result from the slot's durable partial output, details, and diagnostics, ending with `code`; and the
|
|
790
|
+
* truncation diagnostic of that output, when its bounds dropped some, which the caller adds after `afterTool`.
|
|
791
|
+
*/
|
|
678
792
|
function fromSlot(slot, code, message) {
|
|
679
|
-
const diagnostics = [...(slot?.diagnostics ?? [])];
|
|
680
793
|
const droppedBytes = slot?.droppedBytes ?? 0;
|
|
681
|
-
|
|
682
|
-
diagnostics.push(truncated({ droppedBytes, droppedLines: slot?.droppedLines ?? 0 }));
|
|
683
|
-
diagnostics.push(toolDiagnostic(code, message));
|
|
684
|
-
return {
|
|
794
|
+
const result = {
|
|
685
795
|
output: slot?.output === undefined || slot.output === "" ? [] : [{ type: "text", text: slot.output }],
|
|
686
796
|
isError: true,
|
|
687
797
|
...(slot?.details === undefined ? {} : { details: slot.details }),
|
|
688
|
-
diagnostics,
|
|
798
|
+
diagnostics: [...(slot?.diagnostics ?? []), toolDiagnostic(code, message)],
|
|
689
799
|
};
|
|
800
|
+
if (droppedBytes === 0)
|
|
801
|
+
return { result };
|
|
802
|
+
return { result, truncation: truncated({ droppedBytes, droppedLines: slot?.droppedLines ?? 0 }) };
|
|
690
803
|
}
|
|
691
804
|
/** An error result the Harness writes itself: no content and one `error` diagnostic with `code`. */
|
|
692
805
|
export function harnessError(code, message) {
|
|
@@ -706,9 +819,12 @@ function truncated(dropped, retain) {
|
|
|
706
819
|
}
|
|
707
820
|
/** Error text a nested call's summary keeps. */
|
|
708
821
|
const MAX_SUMMARY_ERROR_CHARS = 500;
|
|
709
|
-
/**
|
|
822
|
+
/**
|
|
823
|
+
* How a nested call ended, for its slot: error text, from its diagnostics or else its `output`, bounded. The bound is a
|
|
824
|
+
* copy, so the slot never keeps a large output alive through a slice of it.
|
|
825
|
+
*/
|
|
710
826
|
function summaryOf(result, output) {
|
|
711
|
-
const error = result.isError ? errorTextOf(result, output)
|
|
827
|
+
const error = result.isError ? ownedSlice(errorTextOf(result, output), 0, MAX_SUMMARY_ERROR_CHARS) : "";
|
|
712
828
|
return {
|
|
713
829
|
isError: result.isError,
|
|
714
830
|
...(result.durationMs === undefined ? {} : { durationMs: result.durationMs }),
|