@code-yeongyu/senpi-agent-core 2026.9.29 → 2026.10.1-2
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/README.md +49 -20
- package/dist/agent-loop.d.ts +27 -3
- package/dist/agent-loop.js +192 -56
- package/dist/agent.d.ts +21 -7
- package/dist/agent.js +38 -18
- package/dist/harness/execution/tools.d.ts +1 -1
- package/dist/harness/execution/tools.js +0 -2
- package/dist/harness/messages.js +1 -0
- package/dist/harness/pico3/bash.d.ts +10 -0
- package/dist/harness/pico3/bash.js +25 -0
- package/dist/harness/pico3/bounded.d.ts +21 -0
- package/dist/harness/pico3/bounded.js +95 -0
- package/dist/harness/pico3/chord.d.ts +41 -0
- package/dist/harness/pico3/chord.js +147 -0
- package/dist/harness/pico3/context.d.ts +16 -0
- package/dist/harness/pico3/context.js +88 -0
- package/dist/harness/pico3/harness.d.ts +198 -0
- package/dist/harness/pico3/harness.js +645 -0
- package/dist/harness/pico3/hooks.d.ts +10 -0
- package/dist/harness/pico3/hooks.js +33 -0
- package/dist/harness/pico3/index.d.ts +19 -0
- package/dist/harness/pico3/index.js +15 -0
- package/dist/harness/pico3/jsonl.d.ts +58 -0
- package/dist/harness/pico3/jsonl.js +322 -0
- package/dist/harness/pico3/kinds/collapse.d.ts +49 -0
- package/dist/harness/pico3/kinds/collapse.js +191 -0
- package/dist/harness/pico3/kinds/entries.d.ts +78 -0
- package/dist/harness/pico3/kinds/entries.js +13 -0
- package/dist/harness/pico3/kinds/frames.d.ts +9 -0
- package/dist/harness/pico3/kinds/frames.js +75 -0
- package/dist/harness/pico3/kinds/generation.d.ts +94 -0
- package/dist/harness/pico3/kinds/generation.js +504 -0
- package/dist/harness/pico3/kinds/job.d.ts +44 -0
- package/dist/harness/pico3/kinds/job.js +143 -0
- package/dist/harness/pico3/kinds/plugin.d.ts +15 -0
- package/dist/harness/pico3/kinds/plugin.js +35 -0
- package/dist/harness/pico3/kinds/post-tools.d.ts +22 -0
- package/dist/harness/pico3/kinds/post-tools.js +145 -0
- package/dist/harness/pico3/kinds/task-api.d.ts +3 -0
- package/dist/harness/pico3/kinds/task-api.js +45 -0
- package/dist/harness/pico3/kinds/tool.d.ts +43 -0
- package/dist/harness/pico3/kinds/tool.js +373 -0
- package/dist/harness/pico3/legacy-tracker.d.ts +11 -0
- package/dist/harness/pico3/legacy-tracker.js +39 -0
- package/dist/harness/pico3/membrane.d.ts +23 -0
- package/dist/harness/pico3/membrane.js +140 -0
- package/dist/harness/pico3/memory.d.ts +53 -0
- package/dist/harness/pico3/memory.js +265 -0
- package/dist/harness/pico3/scheduler.d.ts +49 -0
- package/dist/harness/pico3/scheduler.js +437 -0
- package/dist/harness/pico3/session.d.ts +264 -0
- package/dist/harness/pico3/session.js +1328 -0
- package/dist/harness/pico3/system.d.ts +126 -0
- package/dist/harness/pico3/system.js +244 -0
- package/dist/harness/pico3/types.d.ts +973 -0
- package/dist/harness/pico3/types.js +94 -0
- package/dist/harness/pico3/view.d.ts +34 -0
- package/dist/harness/pico3/view.js +404 -0
- package/dist/harness/runtime/drive/tool-placement.js +2 -27
- package/dist/harness/telemetry.d.ts +36 -36
- package/dist/harness/tools/image.js +1 -1
- package/dist/harness/utils/read-folders/index.d.ts +2 -2
- package/dist/harness/utils/read-folders/index.js +2 -2
- package/dist/harness/utils/read-folders/tree-sitter/grammar-assets.js +1 -1
- package/dist/proxy.d.ts +2 -2
- package/dist/types.d.ts +111 -34
- package/package.json +10 -5
- package/dist/agent-loop.d.ts.map +0 -1
- package/dist/agent-loop.js.map +0 -1
- package/dist/agent.d.ts.map +0 -1
- package/dist/agent.js.map +0 -1
- package/dist/assistant-terminal-state.d.ts.map +0 -1
- package/dist/assistant-terminal-state.js.map +0 -1
- package/dist/empty-assistant-recovery.d.ts.map +0 -1
- package/dist/empty-assistant-recovery.js.map +0 -1
- package/dist/harness/agent-harness.d.ts.map +0 -1
- package/dist/harness/agent-harness.js.map +0 -1
- package/dist/harness/compaction/branch-summarization.d.ts.map +0 -1
- package/dist/harness/compaction/branch-summarization.js.map +0 -1
- package/dist/harness/compaction/compaction.d.ts.map +0 -1
- package/dist/harness/compaction/compaction.js.map +0 -1
- package/dist/harness/compaction/utils.d.ts.map +0 -1
- package/dist/harness/compaction/utils.js.map +0 -1
- package/dist/harness/config.d.ts.map +0 -1
- package/dist/harness/config.js.map +0 -1
- package/dist/harness/context.d.ts.map +0 -1
- package/dist/harness/context.js.map +0 -1
- package/dist/harness/env/nodejs.d.ts.map +0 -1
- package/dist/harness/env/nodejs.js.map +0 -1
- package/dist/harness/events.d.ts.map +0 -1
- package/dist/harness/events.js.map +0 -1
- package/dist/harness/execution/assistant.d.ts.map +0 -1
- package/dist/harness/execution/assistant.js.map +0 -1
- package/dist/harness/execution/effect-gate.d.ts.map +0 -1
- package/dist/harness/execution/effect-gate.js.map +0 -1
- package/dist/harness/execution/tools.d.ts.map +0 -1
- package/dist/harness/execution/tools.js.map +0 -1
- package/dist/harness/hooks.d.ts.map +0 -1
- package/dist/harness/hooks.js.map +0 -1
- package/dist/harness/messages.d.ts.map +0 -1
- package/dist/harness/messages.js.map +0 -1
- package/dist/harness/prompt-templates.d.ts.map +0 -1
- package/dist/harness/prompt-templates.js.map +0 -1
- package/dist/harness/result.d.ts.map +0 -1
- package/dist/harness/result.js.map +0 -1
- package/dist/harness/runtime/drive/boundary.d.ts.map +0 -1
- package/dist/harness/runtime/drive/boundary.js.map +0 -1
- package/dist/harness/runtime/drive/checkpoint.d.ts.map +0 -1
- package/dist/harness/runtime/drive/checkpoint.js.map +0 -1
- package/dist/harness/runtime/drive/deferred.d.ts.map +0 -1
- package/dist/harness/runtime/drive/deferred.js.map +0 -1
- package/dist/harness/runtime/drive/generation.d.ts.map +0 -1
- package/dist/harness/runtime/drive/generation.js.map +0 -1
- package/dist/harness/runtime/drive/reconcile.d.ts.map +0 -1
- package/dist/harness/runtime/drive/reconcile.js.map +0 -1
- package/dist/harness/runtime/drive/recovery.d.ts.map +0 -1
- package/dist/harness/runtime/drive/recovery.js.map +0 -1
- package/dist/harness/runtime/drive/response.d.ts.map +0 -1
- package/dist/harness/runtime/drive/response.js.map +0 -1
- package/dist/harness/runtime/drive/retry.d.ts.map +0 -1
- package/dist/harness/runtime/drive/retry.js.map +0 -1
- package/dist/harness/runtime/drive/structural.d.ts.map +0 -1
- package/dist/harness/runtime/drive/structural.js.map +0 -1
- package/dist/harness/runtime/drive/terminal.d.ts.map +0 -1
- package/dist/harness/runtime/drive/terminal.js.map +0 -1
- package/dist/harness/runtime/drive/tool-placement.d.ts.map +0 -1
- package/dist/harness/runtime/drive/tool-placement.js.map +0 -1
- package/dist/harness/runtime/drive/tools.d.ts.map +0 -1
- package/dist/harness/runtime/drive/tools.js.map +0 -1
- package/dist/harness/runtime/drive.d.ts.map +0 -1
- package/dist/harness/runtime/drive.js.map +0 -1
- package/dist/harness/runtime/harness.d.ts.map +0 -1
- package/dist/harness/runtime/harness.js.map +0 -1
- package/dist/harness/runtime/index.d.ts.map +0 -1
- package/dist/harness/runtime/index.js.map +0 -1
- package/dist/harness/runtime/lane.d.ts.map +0 -1
- package/dist/harness/runtime/lane.js.map +0 -1
- package/dist/harness/runtime/progress.d.ts.map +0 -1
- package/dist/harness/runtime/progress.js.map +0 -1
- package/dist/harness/runtime/reducer.d.ts.map +0 -1
- package/dist/harness/runtime/reducer.js.map +0 -1
- package/dist/harness/runtime/restore.d.ts.map +0 -1
- package/dist/harness/runtime/restore.js.map +0 -1
- package/dist/harness/runtime/transcript.d.ts.map +0 -1
- package/dist/harness/runtime/transcript.js.map +0 -1
- package/dist/harness/runtime/types.d.ts.map +0 -1
- package/dist/harness/runtime/types.js.map +0 -1
- package/dist/harness/session/commit.d.ts.map +0 -1
- package/dist/harness/session/commit.js.map +0 -1
- package/dist/harness/session/context.d.ts.map +0 -1
- package/dist/harness/session/context.js.map +0 -1
- package/dist/harness/session/fork-policy.d.ts.map +0 -1
- package/dist/harness/session/fork-policy.js.map +0 -1
- package/dist/harness/session/fork.d.ts.map +0 -1
- package/dist/harness/session/fork.js.map +0 -1
- package/dist/harness/session/in-memory-storage-state.d.ts.map +0 -1
- package/dist/harness/session/in-memory-storage-state.js.map +0 -1
- package/dist/harness/session/index.d.ts.map +0 -1
- package/dist/harness/session/index.js.map +0 -1
- package/dist/harness/session/jsonl/codec.d.ts.map +0 -1
- package/dist/harness/session/jsonl/codec.js.map +0 -1
- package/dist/harness/session/jsonl/fork.d.ts.map +0 -1
- package/dist/harness/session/jsonl/fork.js.map +0 -1
- package/dist/harness/session/jsonl/index.d.ts.map +0 -1
- package/dist/harness/session/jsonl/index.js.map +0 -1
- package/dist/harness/session/jsonl/io.d.ts.map +0 -1
- package/dist/harness/session/jsonl/io.js.map +0 -1
- package/dist/harness/session/jsonl/legacy-v3.d.ts.map +0 -1
- package/dist/harness/session/jsonl/legacy-v3.js.map +0 -1
- package/dist/harness/session/jsonl/repo.d.ts.map +0 -1
- package/dist/harness/session/jsonl/repo.js.map +0 -1
- package/dist/harness/session/jsonl/storage.d.ts.map +0 -1
- package/dist/harness/session/jsonl/storage.js.map +0 -1
- package/dist/harness/session/jsonl/types.d.ts.map +0 -1
- package/dist/harness/session/jsonl/types.js.map +0 -1
- package/dist/harness/session/memory.d.ts.map +0 -1
- package/dist/harness/session/memory.js.map +0 -1
- package/dist/harness/session/mutation-line.d.ts.map +0 -1
- package/dist/harness/session/mutation-line.js.map +0 -1
- package/dist/harness/session/session.d.ts.map +0 -1
- package/dist/harness/session/session.js.map +0 -1
- package/dist/harness/session/testing/benchmark/datasets.d.ts.map +0 -1
- package/dist/harness/session/testing/benchmark/datasets.js.map +0 -1
- package/dist/harness/session/testing/benchmark/session-repo.d.ts.map +0 -1
- package/dist/harness/session/testing/benchmark/session-repo.js.map +0 -1
- package/dist/harness/session/testing/benchmark/storage.d.ts.map +0 -1
- package/dist/harness/session/testing/benchmark/storage.js.map +0 -1
- package/dist/harness/session/testing/conformance/session-repo.d.ts.map +0 -1
- package/dist/harness/session/testing/conformance/session-repo.js.map +0 -1
- package/dist/harness/session/testing/conformance/storage.d.ts.map +0 -1
- package/dist/harness/session/testing/conformance/storage.js.map +0 -1
- package/dist/harness/session/testing/gating-storage.d.ts.map +0 -1
- package/dist/harness/session/testing/gating-storage.js.map +0 -1
- package/dist/harness/session/testing/index.d.ts.map +0 -1
- package/dist/harness/session/testing/index.js.map +0 -1
- package/dist/harness/session/testing/instrumented-storage.d.ts.map +0 -1
- package/dist/harness/session/testing/instrumented-storage.js.map +0 -1
- package/dist/harness/session/testing/storage-decorator.d.ts.map +0 -1
- package/dist/harness/session/testing/storage-decorator.js.map +0 -1
- package/dist/harness/session/testing/types.d.ts.map +0 -1
- package/dist/harness/session/testing/types.js.map +0 -1
- package/dist/harness/session/types.d.ts.map +0 -1
- package/dist/harness/session/types.js.map +0 -1
- package/dist/harness/session/values.d.ts.map +0 -1
- package/dist/harness/session/values.js.map +0 -1
- package/dist/harness/skills.d.ts.map +0 -1
- package/dist/harness/skills.js.map +0 -1
- package/dist/harness/system-prompt.d.ts.map +0 -1
- package/dist/harness/system-prompt.js.map +0 -1
- package/dist/harness/telemetry.d.ts.map +0 -1
- package/dist/harness/telemetry.js.map +0 -1
- package/dist/harness/tools/bash.d.ts.map +0 -1
- package/dist/harness/tools/bash.js.map +0 -1
- package/dist/harness/tools/edit-diff.d.ts.map +0 -1
- package/dist/harness/tools/edit-diff.js.map +0 -1
- package/dist/harness/tools/edit.d.ts.map +0 -1
- package/dist/harness/tools/edit.js.map +0 -1
- package/dist/harness/tools/file-mutation-queue.d.ts.map +0 -1
- package/dist/harness/tools/file-mutation-queue.js.map +0 -1
- package/dist/harness/tools/image.d.ts.map +0 -1
- package/dist/harness/tools/image.js.map +0 -1
- package/dist/harness/tools/index.d.ts.map +0 -1
- package/dist/harness/tools/index.js.map +0 -1
- package/dist/harness/tools/path-utils.d.ts.map +0 -1
- package/dist/harness/tools/path-utils.js.map +0 -1
- package/dist/harness/tools/post-mutate.d.ts.map +0 -1
- package/dist/harness/tools/post-mutate.js.map +0 -1
- package/dist/harness/tools/read.d.ts.map +0 -1
- package/dist/harness/tools/read.js.map +0 -1
- package/dist/harness/tools/tool-context.d.ts.map +0 -1
- package/dist/harness/tools/tool-context.js.map +0 -1
- package/dist/harness/tools/write.d.ts.map +0 -1
- package/dist/harness/tools/write.js.map +0 -1
- package/dist/harness/types.d.ts.map +0 -1
- package/dist/harness/types.js.map +0 -1
- package/dist/harness/utils/adaptive-publisher.d.ts.map +0 -1
- package/dist/harness/utils/adaptive-publisher.js.map +0 -1
- package/dist/harness/utils/output-capture.d.ts.map +0 -1
- package/dist/harness/utils/output-capture.js.map +0 -1
- package/dist/harness/utils/read-folders/brace-scanner.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/brace-scanner.js.map +0 -1
- package/dist/harness/utils/read-folders/compose.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/compose.js.map +0 -1
- package/dist/harness/utils/read-folders/header-protection.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/header-protection.js.map +0 -1
- package/dist/harness/utils/read-folders/index.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/index.js.map +0 -1
- package/dist/harness/utils/read-folders/lexical-context.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/lexical-context.js.map +0 -1
- package/dist/harness/utils/read-folders/lexical-spans.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/lexical-spans.js.map +0 -1
- package/dist/harness/utils/read-folders/prepare.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/prepare.js.map +0 -1
- package/dist/harness/utils/read-folders/tree-sitter/engine.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/tree-sitter/engine.js.map +0 -1
- package/dist/harness/utils/read-folders/tree-sitter/grammar-assets.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/tree-sitter/grammar-assets.js.map +0 -1
- package/dist/harness/utils/read-folders/tree-sitter/syntax.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/tree-sitter/syntax.js.map +0 -1
- package/dist/harness/utils/read-folders/types.d.ts.map +0 -1
- package/dist/harness/utils/read-folders/types.js.map +0 -1
- package/dist/harness/utils/segmented-read-view.d.ts.map +0 -1
- package/dist/harness/utils/segmented-read-view.js.map +0 -1
- package/dist/harness/utils/shell-output.d.ts.map +0 -1
- package/dist/harness/utils/shell-output.js.map +0 -1
- package/dist/harness/utils/truncate.d.ts.map +0 -1
- package/dist/harness/utils/truncate.js.map +0 -1
- package/dist/harness/utils/usage.d.ts.map +0 -1
- package/dist/harness/utils/usage.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/node.d.ts.map +0 -1
- package/dist/node.js.map +0 -1
- package/dist/proxy.d.ts.map +0 -1
- package/dist/proxy.js.map +0 -1
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/stream-fn.d.ts.map +0 -1
- package/dist/stream-fn.js.map +0 -1
- package/dist/tool-arguments.d.ts.map +0 -1
- package/dist/tool-arguments.js.map +0 -1
- package/dist/tool-name-alias.d.ts.map +0 -1
- package/dist/tool-name-alias.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
package/README.md
CHANGED
|
@@ -130,27 +130,54 @@ The `beforeToolCall` hook runs after `tool_execution_start` and validated argume
|
|
|
130
130
|
|
|
131
131
|
Tools, blocked `beforeToolCall` results, and `afterToolCall` overrides can return `terminate: true` to hint that the automatic follow-up LLM call should be skipped. The loop only stops early when every finalized tool result in that batch sets `terminate: true`. Mixed batches continue normally.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
When you use the `Agent` class, assistant `message_end` processing is treated as a barrier before tool preflight begins. That means `beforeToolCall` sees agent state that already includes the assistant message that requested the tool call.
|
|
134
|
+
|
|
135
|
+
### Request preparation and turn finalization
|
|
136
|
+
|
|
137
|
+
`prepareRequest` runs immediately before every conversational provider request, including the first. Use it to install canonical persisted context after pending input has been emitted:
|
|
134
138
|
|
|
135
139
|
```typescript
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{
|
|
140
|
-
model,
|
|
141
|
-
convertToLlm,
|
|
142
|
-
shouldStopAfterTurn: async ({ message, toolResults, context, newMessages }) => {
|
|
143
|
-
return shouldCompactBeforeNextTurn(context.messages);
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
undefined,
|
|
147
|
-
models.streamSimple.bind(models),
|
|
148
|
-
);
|
|
140
|
+
agent.prepareRequest = async ({ context }) => ({
|
|
141
|
+
context: { ...context, messages: await session.loadModelContext() },
|
|
142
|
+
});
|
|
149
143
|
```
|
|
150
144
|
|
|
151
|
-
`
|
|
145
|
+
`prepareRequest` does not poll queues. Steering queued while it runs waits for the next normal steering poll.
|
|
152
146
|
|
|
153
|
-
|
|
147
|
+
`finishTurn` runs after the assistant message and all tool results are finalized, but before `turn_end`. It runs for normal, error, and aborted responses:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
agent.finishTurn = async ({ message }) => {
|
|
151
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") return;
|
|
152
|
+
if (shouldEndRun(message)) return { action: "end" };
|
|
153
|
+
return needsAnotherResponse(message) ? { action: "continue" } : undefined;
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Returning `undefined` keeps normal scheduling. `{ action: "end" }` stops right after `turn_end`, before polling steering or follow-up queues or preparing another request. On a normal response, `{ action: "continue" }` makes sure one more provider request happens: tool results, steering, or a follow-up that already cause that request satisfy it, and otherwise the loop makes one context-only request. Error and aborted responses stay hard exits, so their decisions are ignored. `finishTurn` runs again after the next request, so returning `{ action: "continue" }` unconditionally loops forever. Low-level loop callers set the same hooks in `AgentLoopConfig`.
|
|
158
|
+
|
|
159
|
+
To migrate from the removed `shouldStopAfterTurn`, return `{ action: "end" }` and guard error and aborted responses to keep the old normal-response-only invocation:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
finishTurn: async (turn, signal) => {
|
|
163
|
+
if (turn.message.stopReason === "error" || turn.message.stopReason === "aborted") return;
|
|
164
|
+
return (await shouldStop(turn, signal)) ? { action: "end" } : undefined;
|
|
165
|
+
},
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Each provider turn follows this lifecycle:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
selected input events
|
|
172
|
+
→ prepareRequest
|
|
173
|
+
→ provider response
|
|
174
|
+
→ tool results
|
|
175
|
+
→ finishTurn
|
|
176
|
+
→ turn_end
|
|
177
|
+
→ existing continuation scheduling or agent_end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`agent.peekQueuedMessages()` previews the next queue-selected batch, respecting the queue modes, without consuming it.
|
|
154
181
|
|
|
155
182
|
### continue() Event Sequence
|
|
156
183
|
|
|
@@ -247,9 +274,10 @@ const agent = new Agent({
|
|
|
247
274
|
}
|
|
248
275
|
},
|
|
249
276
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
277
|
+
// End the run gracefully after a completed turn, before queued messages are polled.
|
|
278
|
+
finishTurn: async ({ message, context }, signal) => {
|
|
279
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") return;
|
|
280
|
+
return (await shouldCompactBeforeNextTurn(context.messages, signal)) ? { action: "end" } : undefined;
|
|
253
281
|
},
|
|
254
282
|
|
|
255
283
|
// Custom thinking budgets for token-based providers
|
|
@@ -316,7 +344,8 @@ agent.state.tools = [myTool];
|
|
|
316
344
|
agent.toolExecution = "sequential";
|
|
317
345
|
agent.beforeToolCall = async ({ toolCall }) => undefined;
|
|
318
346
|
agent.afterToolCall = async ({ toolCall, result }) => undefined;
|
|
319
|
-
agent.
|
|
347
|
+
agent.prepareRequest = async ({ context }) => undefined;
|
|
348
|
+
agent.finishTurn = async () => undefined;
|
|
320
349
|
agent.state.messages = newMessages; // top-level array is copied
|
|
321
350
|
agent.state.messages.push(message);
|
|
322
351
|
agent.reset();
|
package/dist/agent-loop.d.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Agent loop that works with AgentMessage throughout.
|
|
3
3
|
* Transforms to Message[] only at the LLM call boundary.
|
|
4
4
|
*/
|
|
5
|
-
import { type
|
|
5
|
+
import { type AssistantMessage, EventStream, type TranscriptContext } from "@earendil-works/pi-ai";
|
|
6
6
|
import { prepareAgentToolCallArguments } from "./tool-arguments.ts";
|
|
7
|
-
import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentTool, AgentToolCall, StreamFn } from "./types.ts";
|
|
7
|
+
import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentTool, AgentToolCall, AgentToolCallOutcome, AgentToolResult, StreamFn } from "./types.ts";
|
|
8
8
|
export type AgentEventSink = (event: AgentEvent) => Promise<void> | void;
|
|
9
9
|
/**
|
|
10
10
|
* Start an agent loop with a new prompt message.
|
|
@@ -26,7 +26,10 @@ export declare class StreamStartTimeoutError extends Error {
|
|
|
26
26
|
constructor(timeoutMs: number);
|
|
27
27
|
}
|
|
28
28
|
/** Build the provider context using the same transform and conversion pipeline as an agent request. */
|
|
29
|
-
export declare function buildProviderContext(context: AgentContext, config: Pick<AgentLoopConfig, "convertToLlm" | "transformContext"> & Partial<Pick<AgentLoopConfig, "model">>, signal?: AbortSignal): Promise<
|
|
29
|
+
export declare function buildProviderContext(context: AgentContext, config: Pick<AgentLoopConfig, "convertToLlm" | "transformContext"> & Partial<Pick<AgentLoopConfig, "model">>, signal?: AbortSignal): Promise<TranscriptContext>;
|
|
30
|
+
/** The `beforeToolCall` and `afterToolCall` hooks of {@link AgentLoopConfig}. */
|
|
31
|
+
export type ToolCallHooks = Pick<AgentLoopConfig, "beforeToolCall" | "afterToolCall">;
|
|
32
|
+
type ToolUpdateSink = (partialResult: AgentToolResult<any>) => Promise<void> | void;
|
|
30
33
|
export interface PreparedAgentToolCall {
|
|
31
34
|
toolCall: AgentToolCall;
|
|
32
35
|
tool: AgentTool;
|
|
@@ -34,4 +37,25 @@ export interface PreparedAgentToolCall {
|
|
|
34
37
|
}
|
|
35
38
|
export { prepareAgentToolCallArguments };
|
|
36
39
|
export declare function prepareAgentToolCall(tool: AgentTool, toolCall: AgentToolCall): PreparedAgentToolCall;
|
|
40
|
+
/** Options for {@link runToolCall}. */
|
|
41
|
+
export interface RunToolCallOptions extends ToolCallHooks {
|
|
42
|
+
/** Tools the call resolves against. */
|
|
43
|
+
tools: readonly AgentTool<any>[];
|
|
44
|
+
/** Passed to the hooks as the message that issued the call. */
|
|
45
|
+
assistantMessage: AssistantMessage;
|
|
46
|
+
/** Passed to the hooks as the current agent context. */
|
|
47
|
+
context: AgentContext;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
onUpdate?: ToolUpdateSink;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run one tool call through the same steps as a model-issued call: argument preparation, schema
|
|
53
|
+
* validation, `beforeToolCall`, execution, and `afterToolCall`. Emits no events and adds no
|
|
54
|
+
* messages. Tools that call other tools use this so the hooks (for example permission checks)
|
|
55
|
+
* apply to those calls too.
|
|
56
|
+
*
|
|
57
|
+
* Never rejects for tool failures: unknown tools, validation errors, blocked calls, and thrown
|
|
58
|
+
* errors come back as `isError: true`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function runToolCall(toolCall: AgentToolCall, options: RunToolCallOptions): Promise<AgentToolCallOutcome>;
|
|
37
61
|
//# sourceMappingURL=agent-loop.d.ts.map
|
package/dist/agent-loop.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Agent loop that works with AgentMessage throughout.
|
|
3
3
|
* Transforms to Message[] only at the LLM call boundary.
|
|
4
4
|
*/
|
|
5
|
-
import { EventStream, isCursorExecResolved, supportsAllowedToolChoice, validateToolArguments, } from "@earendil-works/pi-ai";
|
|
5
|
+
import { createInitialSystemMessage, EventStream, getCurrentTools, getToolStateChanges, isCursorExecResolved, normalizeContext, supportsAllowedToolChoice, validateToolArguments, } from "@earendil-works/pi-ai";
|
|
6
6
|
import { createTerminalFailureAssistantMessage, demoteToolUseWithoutToolCalls, isStreamIdleTimeoutError, normalizeTerminalAssistantMessage, promoteStopWithPendingToolCalls, shouldFinalizeIdleAsStop, shouldTerminateAssistantTurn, } from "./assistant-terminal-state.js";
|
|
7
7
|
import { getDefaultStreamFn, withEmptyAssistantRecovery } from "./stream-fn.js";
|
|
8
8
|
import { prepareAgentToolCallArguments } from "./tool-arguments.js";
|
|
@@ -44,16 +44,17 @@ export function agentLoopContinue(context, config, signal, streamFn) {
|
|
|
44
44
|
return stream;
|
|
45
45
|
}
|
|
46
46
|
export async function runAgentLoop(prompts, context, config, emit, signal, streamFn) {
|
|
47
|
-
const
|
|
47
|
+
const initialMessages = declareToolChanges(context, prompts, config.model);
|
|
48
|
+
const newMessages = [...initialMessages];
|
|
48
49
|
const currentContext = {
|
|
49
50
|
...context,
|
|
50
|
-
messages: [...context.messages, ...
|
|
51
|
+
messages: [...context.messages, ...initialMessages],
|
|
51
52
|
};
|
|
52
53
|
await emit({ type: "agent_start" });
|
|
53
54
|
await emit({ type: "turn_start" });
|
|
54
|
-
for (const
|
|
55
|
-
await emit({ type: "message_start", message
|
|
56
|
-
await emit({ type: "message_end", message
|
|
55
|
+
for (const message of initialMessages) {
|
|
56
|
+
await emit({ type: "message_start", message });
|
|
57
|
+
await emit({ type: "message_end", message });
|
|
57
58
|
}
|
|
58
59
|
await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
|
|
59
60
|
return newMessages;
|
|
@@ -100,6 +101,10 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
100
101
|
let firstProviderRequest = true;
|
|
101
102
|
let drainedTerminatingQueue;
|
|
102
103
|
let turnStartAlreadyEmitted = false;
|
|
104
|
+
// Set by a `finishTurn` `{ action: "continue" }`; cleared once a natural request is scheduled.
|
|
105
|
+
let explicitContinuation = false;
|
|
106
|
+
// Messages from `prepareNextTurn`, appended before the next provider request.
|
|
107
|
+
let preparedMessages = [];
|
|
103
108
|
const refreshTerminatingQueueDrain = async () => {
|
|
104
109
|
if (!drainedTerminatingQueue || !config.restorePendingMessages)
|
|
105
110
|
return;
|
|
@@ -129,21 +134,29 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
129
134
|
}
|
|
130
135
|
if (drainedTerminatingQueue) {
|
|
131
136
|
await refreshTerminatingQueueDrain();
|
|
132
|
-
if (pendingMessages.length === 0) {
|
|
137
|
+
if (pendingMessages.length === 0 && !explicitContinuation) {
|
|
133
138
|
await emit({ type: "agent_end", messages: newMessages });
|
|
134
139
|
return;
|
|
135
140
|
}
|
|
136
141
|
drainedTerminatingQueue = undefined;
|
|
137
142
|
}
|
|
138
|
-
// Process
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
// Process prepared and queued messages before the next assistant response.
|
|
144
|
+
for (const message of declareToolChanges(currentContext, [...preparedMessages, ...pendingMessages], config.model)) {
|
|
145
|
+
await emit({ type: "message_start", message });
|
|
146
|
+
await emit({ type: "message_end", message });
|
|
147
|
+
currentContext.messages.push(message);
|
|
148
|
+
newMessages.push(message);
|
|
149
|
+
}
|
|
150
|
+
preparedMessages = [];
|
|
151
|
+
pendingMessages = [];
|
|
152
|
+
const requestUpdate = await config.prepareRequest?.({
|
|
153
|
+
context: currentContext,
|
|
154
|
+
model: config.model,
|
|
155
|
+
thinkingLevel: config.reasoning ?? "off",
|
|
156
|
+
}, signal);
|
|
157
|
+
if (requestUpdate) {
|
|
158
|
+
currentContext = requestUpdate.context ?? currentContext;
|
|
159
|
+
config = applyLoopUpdate(config, requestUpdate);
|
|
147
160
|
}
|
|
148
161
|
// Stream assistant response
|
|
149
162
|
const isInitialProviderRequest = firstProviderRequest;
|
|
@@ -168,6 +181,8 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
168
181
|
toolResults.push(result);
|
|
169
182
|
}
|
|
170
183
|
if (shouldTerminateAssistantTurn(message)) {
|
|
184
|
+
// Hard exit: the decision is ignored, but the hook still sees the finished turn before turn_end.
|
|
185
|
+
await config.finishTurn?.({ message, toolResults, context: currentContext, newMessages }, signal);
|
|
171
186
|
await emit({ type: "turn_end", message, toolResults });
|
|
172
187
|
await emit({ type: "agent_end", messages: newMessages });
|
|
173
188
|
return;
|
|
@@ -191,21 +206,23 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
191
206
|
newMessages.push(result);
|
|
192
207
|
}
|
|
193
208
|
}
|
|
194
|
-
await emit({ type: "turn_end", message, toolResults });
|
|
195
|
-
if (signal?.aborted) {
|
|
196
|
-
await emit({ type: "agent_end", messages: newMessages });
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
209
|
const nextTurnContext = {
|
|
200
210
|
message,
|
|
201
211
|
toolResults,
|
|
202
212
|
context: currentContext,
|
|
203
213
|
newMessages,
|
|
204
214
|
};
|
|
205
|
-
|
|
215
|
+
const decision = await config.finishTurn?.(nextTurnContext, signal);
|
|
216
|
+
await emit({ type: "turn_end", message, toolResults });
|
|
217
|
+
if (signal?.aborted) {
|
|
218
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (decision?.action === "end") {
|
|
206
222
|
await emit({ type: "agent_end", messages: newMessages });
|
|
207
223
|
return;
|
|
208
224
|
}
|
|
225
|
+
explicitContinuation = decision?.action === "continue";
|
|
209
226
|
if (toolBatchTerminated) {
|
|
210
227
|
pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
211
228
|
if (pendingMessages.length > 0)
|
|
@@ -215,14 +232,16 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
215
232
|
if (pendingMessages.length > 0)
|
|
216
233
|
drainedTerminatingQueue = "followUp";
|
|
217
234
|
}
|
|
218
|
-
if (pendingMessages.length === 0) {
|
|
235
|
+
if (pendingMessages.length === 0 && !explicitContinuation) {
|
|
219
236
|
await emit({ type: "agent_end", messages: newMessages });
|
|
220
237
|
return;
|
|
221
238
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
239
|
+
if (pendingMessages.length > 0) {
|
|
240
|
+
// Give queue owners a boundary before preparation refreshes the drained
|
|
241
|
+
// snapshot, so a clear or replacement wins before admission.
|
|
242
|
+
await emit({ type: "turn_start" });
|
|
243
|
+
turnStartAlreadyEmitted = true;
|
|
244
|
+
}
|
|
226
245
|
}
|
|
227
246
|
let nextTurnSnapshot;
|
|
228
247
|
try {
|
|
@@ -235,19 +254,8 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
235
254
|
}
|
|
236
255
|
if (nextTurnSnapshot) {
|
|
237
256
|
currentContext = nextTurnSnapshot.context ?? currentContext;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
model: nextTurnSnapshot.model ?? config.model,
|
|
241
|
-
reasoning: nextTurnSnapshot.thinkingLevel === undefined
|
|
242
|
-
? config.reasoning
|
|
243
|
-
: nextTurnSnapshot.thinkingLevel === "off"
|
|
244
|
-
? undefined
|
|
245
|
-
: nextTurnSnapshot.thinkingLevel,
|
|
246
|
-
thinkingSelection: nextTurnSnapshot.thinkingSelection === undefined
|
|
247
|
-
? config.thinkingSelection
|
|
248
|
-
: (nextTurnSnapshot.thinkingSelection ?? undefined),
|
|
249
|
-
abortServerSideFallback: nextTurnSnapshot.abortServerSideFallback ?? config.abortServerSideFallback,
|
|
250
|
-
};
|
|
257
|
+
preparedMessages = nextTurnSnapshot.messages ?? [];
|
|
258
|
+
config = applyLoopUpdate(config, nextTurnSnapshot);
|
|
251
259
|
}
|
|
252
260
|
if (signal?.aborted) {
|
|
253
261
|
if (drainedTerminatingQueue)
|
|
@@ -257,7 +265,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
257
265
|
}
|
|
258
266
|
if (drainedTerminatingQueue) {
|
|
259
267
|
await refreshTerminatingQueueDrain();
|
|
260
|
-
if (pendingMessages.length === 0) {
|
|
268
|
+
if (pendingMessages.length === 0 && !explicitContinuation) {
|
|
261
269
|
await emit({ type: "agent_end", messages: newMessages });
|
|
262
270
|
return;
|
|
263
271
|
}
|
|
@@ -266,19 +274,115 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
266
274
|
if (!toolBatchTerminated) {
|
|
267
275
|
pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
268
276
|
}
|
|
277
|
+
if (hasMoreToolCalls || pendingMessages.length > 0) {
|
|
278
|
+
explicitContinuation = false;
|
|
279
|
+
}
|
|
269
280
|
}
|
|
270
281
|
// Agent would stop here. Check for follow-up messages.
|
|
271
282
|
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
|
|
272
283
|
if (followUpMessages.length > 0) {
|
|
273
284
|
// Set as pending so inner loop processes them
|
|
285
|
+
explicitContinuation = false;
|
|
274
286
|
pendingMessages = followUpMessages;
|
|
275
287
|
continue;
|
|
276
288
|
}
|
|
289
|
+
// No natural request was selected, so fulfill the continuation decision with one context-only turn.
|
|
290
|
+
if (explicitContinuation) {
|
|
291
|
+
explicitContinuation = false;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
277
294
|
// No more messages, exit
|
|
278
295
|
break;
|
|
279
296
|
}
|
|
280
297
|
await emit({ type: "agent_end", messages: newMessages });
|
|
281
298
|
}
|
|
299
|
+
/** Apply a `prepareNextTurn` or `prepareRequest` update to the loop config for this and later requests. */
|
|
300
|
+
function applyLoopUpdate(config, update) {
|
|
301
|
+
return {
|
|
302
|
+
...config,
|
|
303
|
+
model: update.model ?? config.model,
|
|
304
|
+
reasoning: update.thinkingLevel === undefined
|
|
305
|
+
? config.reasoning
|
|
306
|
+
: update.thinkingLevel === "off"
|
|
307
|
+
? undefined
|
|
308
|
+
: update.thinkingLevel,
|
|
309
|
+
thinkingSelection: update.thinkingSelection === undefined ? config.thinkingSelection : (update.thinkingSelection ?? undefined),
|
|
310
|
+
abortServerSideFallback: update.abortServerSideFallback ?? config.abortServerSideFallback,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Declare tool loadout changes to the model.
|
|
315
|
+
*
|
|
316
|
+
* The provider tools (senpi#2095 `providerTools`) are what the model may call; the transcript's system
|
|
317
|
+
* messages declare them. Before each request the difference becomes `toolsAdded` and `toolsRemoved` on a
|
|
318
|
+
* system message. When a pending system message exists, its tool fields are treated as intent and replaced
|
|
319
|
+
* with the delta between the committed transcript and the provider tools, so replay always yields exactly
|
|
320
|
+
* those tools. Otherwise a new system message is inserted before the first non-system pending message.
|
|
321
|
+
*
|
|
322
|
+
* Fork: `buildProviderContext` folds `context.systemPrompt` and the provider tools into the leading system
|
|
323
|
+
* message through `normalizeContext()`, so that shorthand declaration counts as committed. A context that
|
|
324
|
+
* carries its tools only through that shorthand therefore never gains a duplicate declaration.
|
|
325
|
+
*/
|
|
326
|
+
function declareToolChanges(context, pendingMessages, model) {
|
|
327
|
+
let systemIndex = -1;
|
|
328
|
+
for (let i = pendingMessages.length - 1; i >= 0; i--) {
|
|
329
|
+
if (pendingMessages[i].role === "system") {
|
|
330
|
+
systemIndex = i;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const pending = pendingMessages[systemIndex];
|
|
335
|
+
const baseline = pending
|
|
336
|
+
? pendingMessages.map((message, index) => index === systemIndex ? withToolChanges(pending, NO_CHANGES) : message)
|
|
337
|
+
: pendingMessages;
|
|
338
|
+
const declared = (providerTools(context, model).tools ?? []).map(toProviderToolDeclaration);
|
|
339
|
+
const shorthand = createInitialSystemMessage(context.systemPrompt, declared);
|
|
340
|
+
const committed = shorthand ? [shorthand, ...context.messages, ...baseline] : [...context.messages, ...baseline];
|
|
341
|
+
const delta = getToolStateChanges(getCurrentTools(committed), declared);
|
|
342
|
+
// `getToolStateChanges` compares through upstream `toToolDeclaration`, which drops the fork `freeform` field;
|
|
343
|
+
// announce the fork declaration itself so a delta-added tool keeps its full provider shape.
|
|
344
|
+
const declaredByName = new Map(declared.map((tool) => [tool.name, tool]));
|
|
345
|
+
const changes = {
|
|
346
|
+
toolsAdded: delta.toolsAdded.map((tool) => declaredByName.get(tool.name) ?? tool),
|
|
347
|
+
toolsRemoved: delta.toolsRemoved,
|
|
348
|
+
};
|
|
349
|
+
const unchanged = changes.toolsAdded.length === 0 && changes.toolsRemoved.length === 0;
|
|
350
|
+
if (pending) {
|
|
351
|
+
// Keep the caller's message object when it already declares no tool changes.
|
|
352
|
+
if (unchanged && !pending.toolsAdded?.length && !pending.toolsRemoved?.length)
|
|
353
|
+
return pendingMessages;
|
|
354
|
+
return baseline.map((message, index) => (index === systemIndex ? withToolChanges(pending, changes) : message));
|
|
355
|
+
}
|
|
356
|
+
if (unchanged)
|
|
357
|
+
return pendingMessages;
|
|
358
|
+
const update = withToolChanges({ role: "system", content: "", timestamp: Date.now() }, changes);
|
|
359
|
+
const insertIndex = pendingMessages.findIndex((message) => message.role !== "system");
|
|
360
|
+
const index = insertIndex === -1 ? pendingMessages.length : insertIndex;
|
|
361
|
+
return [...pendingMessages.slice(0, index), update, ...pendingMessages.slice(index)];
|
|
362
|
+
}
|
|
363
|
+
const NO_CHANGES = { toolsAdded: [], toolsRemoved: [] };
|
|
364
|
+
/**
|
|
365
|
+
* The provider-facing declaration of a tool: every `Tool` field (fork `freeform` included) and nothing executable,
|
|
366
|
+
* so transcripts stay cloneable and serializable. Upstream `toToolDeclaration` would drop `freeform`.
|
|
367
|
+
*/
|
|
368
|
+
function toProviderToolDeclaration(tool) {
|
|
369
|
+
return {
|
|
370
|
+
name: tool.name,
|
|
371
|
+
description: tool.description,
|
|
372
|
+
parameters: tool.parameters,
|
|
373
|
+
...(tool.freeform === undefined ? {} : { freeform: tool.freeform }),
|
|
374
|
+
...(tool.constrainedSampling === undefined ? {} : { constrainedSampling: tool.constrainedSampling }),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/** Copy a system message with its tool fields replaced by `changes`; empty lists omit the field. */
|
|
378
|
+
function withToolChanges(message, { toolsAdded, toolsRemoved }) {
|
|
379
|
+
const { toolsAdded: _added, toolsRemoved: _removed, ...rest } = message;
|
|
380
|
+
return {
|
|
381
|
+
...rest,
|
|
382
|
+
...(toolsAdded.length > 0 ? { toolsAdded } : {}),
|
|
383
|
+
...(toolsRemoved.length > 0 ? { toolsRemoved } : {}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
282
386
|
/**
|
|
283
387
|
* senpi#2095: a model that accepts an allowed-tools restriction receives every declared tool plus the
|
|
284
388
|
* callable subset by name; any other model receives the callable tools alone, as before.
|
|
@@ -300,11 +404,13 @@ export async function buildProviderContext(context, config, signal) {
|
|
|
300
404
|
let messages = context.messages;
|
|
301
405
|
if (config.transformContext)
|
|
302
406
|
messages = await config.transformContext(messages, signal);
|
|
303
|
-
|
|
407
|
+
const { tools, activeToolNames } = providerTools(context, config.model);
|
|
408
|
+
return normalizeContext({
|
|
304
409
|
systemPrompt: context.systemPrompt,
|
|
305
410
|
messages: await config.convertToLlm(messages),
|
|
306
|
-
...
|
|
307
|
-
|
|
411
|
+
...(tools === undefined ? {} : { tools: tools.map(toProviderToolDeclaration) }),
|
|
412
|
+
...(activeToolNames === undefined ? {} : { activeToolNames }),
|
|
413
|
+
});
|
|
308
414
|
}
|
|
309
415
|
/**
|
|
310
416
|
* Stream an assistant response from the LLM.
|
|
@@ -367,6 +473,8 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
367
473
|
}
|
|
368
474
|
: {}),
|
|
369
475
|
});
|
|
476
|
+
// Record the requested level, whichever stream function answered.
|
|
477
|
+
const result = async () => Object.assign(await response.result(), { thinkingLevel: config.reasoning ?? "off" });
|
|
370
478
|
const iterator = response[Symbol.asyncIterator]();
|
|
371
479
|
const eventReader = createAssistantEventReader(iterator, streamIdleTimeoutMs, requestAbortController.signal, (error) => requestAbortController.abort(error), config.streamStartTimeoutMs, response);
|
|
372
480
|
try {
|
|
@@ -423,7 +531,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
423
531
|
break;
|
|
424
532
|
case "done":
|
|
425
533
|
case "error": {
|
|
426
|
-
const finalMessage = normalizeTerminalAssistantMessage(await
|
|
534
|
+
const finalMessage = normalizeTerminalAssistantMessage(await result(), event);
|
|
427
535
|
propagateThinkingTiming(finalMessage);
|
|
428
536
|
if (addedPartial) {
|
|
429
537
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
@@ -446,7 +554,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
446
554
|
finally {
|
|
447
555
|
eventReader.dispose();
|
|
448
556
|
}
|
|
449
|
-
const finalMessage = await
|
|
557
|
+
const finalMessage = await result();
|
|
450
558
|
propagateThinkingTiming(finalMessage);
|
|
451
559
|
if (addedPartial) {
|
|
452
560
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
@@ -672,7 +780,7 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
|
|
|
672
780
|
};
|
|
673
781
|
}
|
|
674
782
|
else {
|
|
675
|
-
const executed = await executePreparedToolCall(preparation, signal, emit);
|
|
783
|
+
const executed = await executePreparedToolCall(preparation, signal, emitToolExecutionUpdate(preparation.toolCall, emit));
|
|
676
784
|
finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
|
|
677
785
|
}
|
|
678
786
|
await emitToolExecutionEnd(finalized, emit);
|
|
@@ -767,7 +875,7 @@ async function runPreparedToolCall(currentContext, assistantMessage, preparation
|
|
|
767
875
|
isError: preparation.isError,
|
|
768
876
|
};
|
|
769
877
|
}
|
|
770
|
-
const executed = await executePreparedToolCall(preparation, signal, emit);
|
|
878
|
+
const executed = await executePreparedToolCall(preparation, signal, emitToolExecutionUpdate(preparation.toolCall, emit));
|
|
771
879
|
return finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
|
|
772
880
|
}
|
|
773
881
|
function isSequentialToolCall(currentContext, toolCall) {
|
|
@@ -884,20 +992,42 @@ function abortReleasePromise(signal) {
|
|
|
884
992
|
signal.addEventListener("abort", () => resolve(ABORTED), { once: true });
|
|
885
993
|
});
|
|
886
994
|
}
|
|
887
|
-
|
|
995
|
+
function emitToolExecutionUpdate(toolCall, emit) {
|
|
996
|
+
return (partialResult) => emit({
|
|
997
|
+
type: "tool_execution_update",
|
|
998
|
+
toolCallId: toolCall.id,
|
|
999
|
+
toolName: toolCall.name,
|
|
1000
|
+
args: toolCall.arguments,
|
|
1001
|
+
partialResult,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Run one tool call through the same steps as a model-issued call: argument preparation, schema
|
|
1006
|
+
* validation, `beforeToolCall`, execution, and `afterToolCall`. Emits no events and adds no
|
|
1007
|
+
* messages. Tools that call other tools use this so the hooks (for example permission checks)
|
|
1008
|
+
* apply to those calls too.
|
|
1009
|
+
*
|
|
1010
|
+
* Never rejects for tool failures: unknown tools, validation errors, blocked calls, and thrown
|
|
1011
|
+
* errors come back as `isError: true`.
|
|
1012
|
+
*/
|
|
1013
|
+
export async function runToolCall(toolCall, options) {
|
|
1014
|
+
const { assistantMessage, context, signal } = options;
|
|
1015
|
+
const tool = options.tools.find((candidate) => candidate.name === toolCall.name);
|
|
1016
|
+
const preparation = await prepareToolCall(context, assistantMessage, toolCall, tool, options, signal);
|
|
1017
|
+
if (preparation.kind === "immediate") {
|
|
1018
|
+
return { toolCall, result: preparation.result, isError: preparation.isError };
|
|
1019
|
+
}
|
|
1020
|
+
const executed = await executePreparedToolCall(preparation, signal, options.onUpdate ?? (() => { }));
|
|
1021
|
+
return finalizeExecutedToolCall(context, assistantMessage, preparation, executed, options, signal);
|
|
1022
|
+
}
|
|
1023
|
+
async function executePreparedToolCall(prepared, signal, onUpdate) {
|
|
888
1024
|
const updateEvents = [];
|
|
889
1025
|
let acceptingUpdates = true;
|
|
890
1026
|
try {
|
|
891
1027
|
const execution = prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
|
|
892
1028
|
if (!acceptingUpdates)
|
|
893
1029
|
return;
|
|
894
|
-
updateEvents.push(Promise.resolve(
|
|
895
|
-
type: "tool_execution_update",
|
|
896
|
-
toolCallId: prepared.toolCall.id,
|
|
897
|
-
toolName: prepared.toolCall.name,
|
|
898
|
-
args: prepared.toolCall.arguments,
|
|
899
|
-
partialResult,
|
|
900
|
-
})));
|
|
1030
|
+
updateEvents.push(Promise.resolve(onUpdate(partialResult)));
|
|
901
1031
|
});
|
|
902
1032
|
const abortRelease = abortReleasePromise(signal);
|
|
903
1033
|
const settled = abortRelease ? await Promise.race([execution, abortRelease]) : await execution;
|
|
@@ -937,6 +1067,8 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
|
|
|
937
1067
|
context: currentContext,
|
|
938
1068
|
}, signal);
|
|
939
1069
|
if (afterResult) {
|
|
1070
|
+
// Structured content not replaced along with the content may no longer match it.
|
|
1071
|
+
const structuredContent = afterResult.structuredContent ?? (afterResult.content ? undefined : result.structuredContent);
|
|
940
1072
|
result = {
|
|
941
1073
|
...result,
|
|
942
1074
|
content: afterResult.content ?? result.content,
|
|
@@ -944,6 +1076,10 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
|
|
|
944
1076
|
usage: afterResult.usage ?? result.usage,
|
|
945
1077
|
terminate: afterResult.terminate ?? result.terminate,
|
|
946
1078
|
};
|
|
1079
|
+
if (structuredContent === undefined)
|
|
1080
|
+
delete result.structuredContent;
|
|
1081
|
+
else
|
|
1082
|
+
result.structuredContent = structuredContent;
|
|
947
1083
|
isError = afterResult.isError ?? isError;
|
|
948
1084
|
}
|
|
949
1085
|
}
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import type { AfterToolCallContext, AfterToolCallResult, AgentContext, AgentEvent, AgentLoopConfig, AgentLoopTurnUpdate, AgentMessage, AgentState, BeforeToolCallContext, BeforeToolCallResult, PrepareNextTurnContext,
|
|
1
|
+
import { type ImageContent, type Message, type SimpleStreamOptions, type ThinkingBudgets, type TranscriptContext, type Transport } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { AfterToolCallContext, AfterToolCallResult, AgentContext, AgentEvent, AgentLoopConfig, AgentLoopTurnUpdate, AgentMessage, AgentState, BeforeToolCallContext, BeforeToolCallResult, FinishTurn, PrepareNextTurnContext, PrepareRequest, QueueMode, StreamFn, ToolExecutionMode } from "./types.ts";
|
|
3
3
|
export type { QueueMode } from "./types.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Initial state for {@link Agent}. Fork: `systemPrompt` and `tools` stay the agent's own state and are folded
|
|
6
|
+
* into the leading system message per request by `buildProviderContext`; they are not copied into `messages`.
|
|
7
|
+
*/
|
|
8
|
+
export type AgentInitialState = Partial<Omit<AgentState, "pendingToolCalls" | "isStreaming" | "streamingMessage" | "errorMessage" | "providerDiagnostic">>;
|
|
4
9
|
/** Options for constructing an {@link Agent}. */
|
|
5
10
|
export interface AgentOptions {
|
|
6
|
-
initialState?:
|
|
11
|
+
initialState?: AgentInitialState;
|
|
7
12
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
8
13
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
9
14
|
streamFn: StreamFn;
|
|
10
15
|
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
|
11
16
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
12
17
|
onResponse?: SimpleStreamOptions["onResponse"];
|
|
18
|
+
onProviderStreamEvent?: SimpleStreamOptions["onProviderStreamEvent"];
|
|
13
19
|
beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;
|
|
14
20
|
afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;
|
|
15
|
-
|
|
21
|
+
finishTurn?: FinishTurn;
|
|
22
|
+
prepareRequest?: PrepareRequest;
|
|
16
23
|
prepareNextTurn?: (signal?: AbortSignal) => Promise<AgentLoopTurnUpdate | undefined> | AgentLoopTurnUpdate | undefined;
|
|
17
24
|
prepareNextTurnWithContext?: (context: PrepareNextTurnContext, signal?: AbortSignal) => Promise<AgentLoopTurnUpdate | undefined> | AgentLoopTurnUpdate | undefined;
|
|
18
25
|
steeringMode?: QueueMode;
|
|
@@ -55,9 +62,11 @@ export declare class Agent {
|
|
|
55
62
|
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
|
56
63
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
57
64
|
onResponse?: SimpleStreamOptions["onResponse"];
|
|
65
|
+
onProviderStreamEvent?: SimpleStreamOptions["onProviderStreamEvent"];
|
|
58
66
|
beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;
|
|
59
67
|
afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;
|
|
60
|
-
|
|
68
|
+
finishTurn?: FinishTurn;
|
|
69
|
+
prepareRequest?: PrepareRequest;
|
|
61
70
|
prepareNextTurn?: (signal?: AbortSignal) => Promise<AgentLoopTurnUpdate | undefined> | AgentLoopTurnUpdate | undefined;
|
|
62
71
|
prepareNextTurnWithContext?: (context: PrepareNextTurnContext, signal?: AbortSignal) => Promise<AgentLoopTurnUpdate | undefined> | AgentLoopTurnUpdate | undefined;
|
|
63
72
|
private activeRun?;
|
|
@@ -82,7 +91,7 @@ export declare class Agent {
|
|
|
82
91
|
abortServerSideFallback?: boolean;
|
|
83
92
|
/** Cursor exec-channel tool handlers; see {@link AgentLoopConfig.cursorExecHandlers}. */
|
|
84
93
|
cursorExecHandlers?: AgentLoopConfig["cursorExecHandlers"];
|
|
85
|
-
buildProviderContext(context: AgentContext, signal?: AbortSignal): Promise<
|
|
94
|
+
buildProviderContext(context: AgentContext, signal?: AbortSignal): Promise<TranscriptContext>;
|
|
86
95
|
constructor(options: AgentOptions);
|
|
87
96
|
/**
|
|
88
97
|
* Subscribe to agent lifecycle events.
|
|
@@ -119,6 +128,8 @@ export declare class Agent {
|
|
|
119
128
|
clearAllQueues(): void;
|
|
120
129
|
/** Returns true when either queue still contains pending messages. */
|
|
121
130
|
hasQueuedMessages(): boolean;
|
|
131
|
+
/** Preview the messages selected for the next turn without consuming them. */
|
|
132
|
+
peekQueuedMessages(): AgentMessage[];
|
|
122
133
|
/** Active abort signal for the current run, if any. */
|
|
123
134
|
get signal(): AbortSignal | undefined;
|
|
124
135
|
/** Abort the current run, if one is active. */
|
|
@@ -136,7 +147,10 @@ export declare class Agent {
|
|
|
136
147
|
* This resolves after `agent_end` listeners settle.
|
|
137
148
|
*/
|
|
138
149
|
waitForIdle(): Promise<void>;
|
|
139
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Clear transcript state, runtime state, and queued messages. Fork: the prompt/tool baseline is agent state
|
|
152
|
+
* (`systemPrompt`, `tools`), so transcript system messages are conversation content and are cleared too.
|
|
153
|
+
*/
|
|
140
154
|
reset(): void;
|
|
141
155
|
/** Start a new prompt from text, a single message, or a batch of messages. */
|
|
142
156
|
prompt(message: AgentMessage | AgentMessage[]): Promise<void>;
|