@fastagent-sh/fastagent 0.24.1 → 0.24.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.
|
@@ -11,6 +11,7 @@ import { resolveModel } from "./config.js";
|
|
|
11
11
|
import { activePath, resolveSessionSettings } from "./session-settings.js";
|
|
12
12
|
import { DEFAULT_THINKING_LEVEL, withModelRegistration } from "./models.js";
|
|
13
13
|
import { registerAccountModels } from "./openai-account-models.js";
|
|
14
|
+
import { asksToEndRun, markEndsRun } from "./turn-kit.js";
|
|
14
15
|
import { agentSessionManager, sessionToolActivation, turnContext } from "./tool-context.js";
|
|
15
16
|
/** fastagent's tools as pi tool definitions, bound to ONE session. */
|
|
16
17
|
function toolDefinitions(tools, bound, sessionId) {
|
|
@@ -161,7 +162,7 @@ const nativeExtensions = BUILTIN_EXTENSIONS.map((name) => ({
|
|
|
161
162
|
/** The resource posture a fastagent definition asks pi for — ONE definition of it, for both assemblies. */
|
|
162
163
|
export function definitionResourceLoaderOptions(source) {
|
|
163
164
|
return {
|
|
164
|
-
extensionFactories: [...nativeExtensions, compactAdmission],
|
|
165
|
+
extensionFactories: [...nativeExtensions, compactAdmission, recordEndsRun],
|
|
165
166
|
// The machine's extensions are its owner's setup, not this agent's.
|
|
166
167
|
noExtensions: true,
|
|
167
168
|
// Explicit paths survive noExtensions, including Pi's built-ins; so they would also survive the machine's own
|
|
@@ -193,6 +194,29 @@ export function definitionResourceLoaderOptions(source) {
|
|
|
193
194
|
promptsOverride: () => ({ prompts: [...source.machine.prompts], diagnostics: [] }),
|
|
194
195
|
};
|
|
195
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Records on a tool result that it asked to end its run ({@link asksToEndRun}), so `entries()` can tell a run that
|
|
199
|
+
* ended on its tool batch from one cut after its tools. Through pi's `message_end` replacement, which runs before pi
|
|
200
|
+
* appends the message to the record. LAST among the extensions (pi appends inline factories after path ones), so a
|
|
201
|
+
* definition's own `message_end` replacement cannot drop the flag. One instance per loader, so per session.
|
|
202
|
+
*/
|
|
203
|
+
const recordEndsRun = {
|
|
204
|
+
name: "fastagent-record-ends-run",
|
|
205
|
+
hidden: true,
|
|
206
|
+
factory: (pi) => {
|
|
207
|
+
const asked = new Set();
|
|
208
|
+
pi.on("tool_execution_end", (event) => {
|
|
209
|
+
if (asksToEndRun(event))
|
|
210
|
+
asked.add(event.toolCallId);
|
|
211
|
+
});
|
|
212
|
+
pi.on("message_end", (event) => {
|
|
213
|
+
const message = event.message;
|
|
214
|
+
if (message.role !== "toolResult" || !asked.delete(message.toolCallId))
|
|
215
|
+
return;
|
|
216
|
+
return { message: markEndsRun(message) };
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
};
|
|
196
220
|
/** Manual compactions waiting for pi's admission, keyed by the session's record (what an extension's `ctx` names). */
|
|
197
221
|
const compactAdmissions = new WeakMap();
|
|
198
222
|
const COMPACT_ADMISSION = "fastagent-compact-admission";
|
|
@@ -10,7 +10,7 @@ import { log } from "../../log.js";
|
|
|
10
10
|
import { toRetryScheduledEvent } from "./retry-event.js";
|
|
11
11
|
import { PortFailure, port, portAbort, portCleanup, portError } from "../../effect-port.js";
|
|
12
12
|
import { SessionBusy, acquireSession, acquireSessionLease } from "./session-effects.js";
|
|
13
|
-
import { errorToTerminal, inProcessLease, agentEventProjection, answerOutcome, toPiPromptOptions, toTerminal, } from "./turn-kit.js";
|
|
13
|
+
import { errorToTerminal, inProcessLease, agentEventProjection, answerOutcome, asksToEndRun, toPiPromptOptions, toTerminal, } from "./turn-kit.js";
|
|
14
14
|
// Structured output is for scripts. Observation publishes Pi's display result, including bounded nested traces.
|
|
15
15
|
function displayToolResult(result) {
|
|
16
16
|
const { structuredContent: _internal, ...display } = result;
|
|
@@ -78,6 +78,7 @@ function toSessionEvent(event, runId) {
|
|
|
78
78
|
isError: event.isError,
|
|
79
79
|
content: displayToolResult(event.result),
|
|
80
80
|
...(event.parentToolCallId ? { parentToolCallId: event.parentToolCallId } : {}),
|
|
81
|
+
...(asksToEndRun(event) ? { terminate: true } : {}),
|
|
81
82
|
},
|
|
82
83
|
};
|
|
83
84
|
case "queue_update":
|
|
@@ -12,7 +12,7 @@ import { SESSION_BUSY_CODE } from "../../agent.js";
|
|
|
12
12
|
import { BOUNDARY_COMMAND_FAILED_CODE, INVALID_COMMAND_CODE, isAddressableSession, NO_ACTIVE_RUN_CODE, NOTHING_TO_COMPACT_CODE, NO_SUCH_SESSION_CODE, PARTIAL_UPDATE_CODE, UPDATE_FIELDS, RUN_COMMAND_FAILED_CODE, UNSUPPORTED_CAPABILITY_CODE, } from "../../session.js";
|
|
13
13
|
import { forkProvenance, isConversationMessage, isNavigable, publishedLeaf } from "./session-markers.js";
|
|
14
14
|
import { entryImages, imageAt } from "./entry-images.js";
|
|
15
|
-
import { answerOutcome } from "./turn-kit.js";
|
|
15
|
+
import { answerOutcome, endsRun } from "./turn-kit.js";
|
|
16
16
|
import { startCompaction } from "./agent-session-factory.js";
|
|
17
17
|
import { toRetryScheduledEvent } from "./retry-event.js";
|
|
18
18
|
import { THINKING_LEVELS, activePath, describeModels, resolveSessionSettings } from "./session-settings.js";
|
|
@@ -107,6 +107,8 @@ function toSessionEntry(entry, parentId) {
|
|
|
107
107
|
isError: m.isError ?? false,
|
|
108
108
|
text: contentText(m.content, ""),
|
|
109
109
|
...entryImages(entry.id, m),
|
|
110
|
+
// Same flag as the live `tool_finished` (turn-kit.ts asksToEndRun).
|
|
111
|
+
...(endsRun(m) ? { terminate: true } : {}),
|
|
110
112
|
},
|
|
111
113
|
};
|
|
112
114
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AssistantMessage, type ImageContent } from "@earendil-works/pi-ai";
|
|
1
|
+
import { type AssistantMessage, type ImageContent, type ToolResultMessage } from "@earendil-works/pi-ai";
|
|
2
2
|
import { type AgentEvent, type Prompt } from "../../agent.ts";
|
|
3
3
|
import type { AnswerOutcome, PromptDisposition, SessionEvent } from "../../session.ts";
|
|
4
4
|
export type Release = () => void;
|
|
@@ -18,6 +18,25 @@ export declare function classifyRetryable(details: string, signal: {
|
|
|
18
18
|
* the recorded one cannot disagree.
|
|
19
19
|
*/
|
|
20
20
|
export declare function answerOutcome(message: AssistantMessage): AnswerOutcome | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a finished tool call asked to end its run (pi's `terminate`: returned by the tool, or set by a `tool_call`
|
|
23
|
+
* hook that blocks it; pi 0.99.2's `tool_result` hook cannot set it). After a batch whose EVERY result asks it, pi
|
|
24
|
+
* makes no further model call for that batch, so unless a queued steer or follow-up continues the run, it completes on
|
|
25
|
+
* `tool` entries with no answer after them: the same shape a process killed after its tools leaves. A call made from
|
|
26
|
+
* inside another tool (`parentToolCallId`, a codemode script) is not in the batch and does not count.
|
|
27
|
+
* The ONE reading: the live `tool_finished` and the flag {@link markEndsRun} records both come from it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function asksToEndRun(event: {
|
|
30
|
+
result?: unknown;
|
|
31
|
+
parentToolCallId?: string;
|
|
32
|
+
}): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The tool result message as pi then records it, carrying the request: pi's `createToolResultMessage` drops
|
|
35
|
+
* `terminate` (as of pi 0.99.2), so without this nothing in the record says the run ended on purpose.
|
|
36
|
+
*/
|
|
37
|
+
export declare function markEndsRun(message: ToolResultMessage): ToolResultMessage;
|
|
38
|
+
/** Whether a recorded tool result carries {@link markEndsRun}'s flag. */
|
|
39
|
+
export declare function endsRun(message: ToolResultMessage): boolean;
|
|
21
40
|
/**
|
|
22
41
|
* Terminal mapping, decided by the resolved message's outcome: pi's `prompt()` RESOLVES a message with stopReason
|
|
23
42
|
* "error"/"aborted" rather than throwing, so relying on catch alone would miss that whole failure class and violate
|
|
@@ -91,6 +91,28 @@ export function answerOutcome(message) {
|
|
|
91
91
|
return undefined;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether a finished tool call asked to end its run (pi's `terminate`: returned by the tool, or set by a `tool_call`
|
|
96
|
+
* hook that blocks it; pi 0.99.2's `tool_result` hook cannot set it). After a batch whose EVERY result asks it, pi
|
|
97
|
+
* makes no further model call for that batch, so unless a queued steer or follow-up continues the run, it completes on
|
|
98
|
+
* `tool` entries with no answer after them: the same shape a process killed after its tools leaves. A call made from
|
|
99
|
+
* inside another tool (`parentToolCallId`, a codemode script) is not in the batch and does not count.
|
|
100
|
+
* The ONE reading: the live `tool_finished` and the flag {@link markEndsRun} records both come from it.
|
|
101
|
+
*/
|
|
102
|
+
export function asksToEndRun(event) {
|
|
103
|
+
return !event.parentToolCallId && event.result?.terminate === true;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The tool result message as pi then records it, carrying the request: pi's `createToolResultMessage` drops
|
|
107
|
+
* `terminate` (as of pi 0.99.2), so without this nothing in the record says the run ended on purpose.
|
|
108
|
+
*/
|
|
109
|
+
export function markEndsRun(message) {
|
|
110
|
+
return { ...message, terminate: true };
|
|
111
|
+
}
|
|
112
|
+
/** Whether a recorded tool result carries {@link markEndsRun}'s flag. */
|
|
113
|
+
export function endsRun(message) {
|
|
114
|
+
return message.terminate === true;
|
|
115
|
+
}
|
|
94
116
|
/**
|
|
95
117
|
* Terminal mapping, decided by the resolved message's outcome: pi's `prompt()` RESOLVES a message with stopReason
|
|
96
118
|
* "error"/"aborted" rather than throwing, so relying on catch alone would miss that whole failure class and violate
|
package/dist/session.d.ts
CHANGED
|
@@ -388,11 +388,17 @@ export type ToolProgressEvent = SessionEvent<"tool_progress", {
|
|
|
388
388
|
}> & {
|
|
389
389
|
runId: string;
|
|
390
390
|
};
|
|
391
|
+
/**
|
|
392
|
+
* `terminate: true` when the call asked to end its run. Once every call answering one assistant message asks it, the
|
|
393
|
+
* model is not called again for them, and the run ends there with no answer after its `tool` entries (which carry the
|
|
394
|
+
* same flag) unless a queued steer or follow-up continues it. Whether the run ended is `run_settled`'s to say.
|
|
395
|
+
*/
|
|
391
396
|
export type ToolFinishedEvent = SessionEvent<"tool_finished", {
|
|
392
397
|
id: string;
|
|
393
398
|
isError: boolean;
|
|
394
399
|
content: Json;
|
|
395
400
|
parentToolCallId?: string;
|
|
401
|
+
terminate?: true;
|
|
396
402
|
}> & {
|
|
397
403
|
runId: string;
|
|
398
404
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastagent-sh/fastagent",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.2",
|
|
4
4
|
"description": "Vibe first. Then FastAgent: turn a local agent directory into a live service in your app, on Telegram, Slack, Feishu, or behind any channel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|