@fastagent-sh/fastagent 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +10 -0
- package/dist/channels/kit/preview-kit.d.ts +6 -1
- package/dist/channels/kit/preview-kit.js +21 -4
- package/dist/channels/slack/preview.js +2 -1
- package/dist/cli/commands/info.js +7 -7
- package/dist/cli/commands/login.js +2 -2
- package/dist/cli/commands/models.js +3 -3
- package/dist/cli/invoke-stream.js +14 -0
- package/dist/cli/shared.d.ts +2 -1
- package/dist/cli/shared.js +6 -7
- package/dist/deploy/preflight.js +19 -1
- package/dist/dev-supervisor.js +2 -2
- package/dist/engines/pi/agent-models.d.ts +14 -1
- package/dist/engines/pi/agent-models.js +35 -8
- package/dist/engines/pi/agent-session-factory.d.ts +13 -13
- package/dist/engines/pi/agent-session-factory.js +132 -159
- package/dist/engines/pi/auth.js +6 -94
- package/dist/engines/pi/create.d.ts +16 -9
- package/dist/engines/pi/create.js +71 -38
- package/dist/engines/pi/invoke-session.js +82 -16
- package/dist/engines/pi/locked-file.d.ts +9 -0
- package/dist/engines/pi/locked-file.js +102 -0
- package/dist/engines/pi/login.d.ts +7 -1
- package/dist/engines/pi/login.js +22 -6
- package/dist/engines/pi/machine.d.ts +23 -0
- package/dist/engines/pi/machine.js +24 -2
- package/dist/engines/pi/models.d.ts +47 -42
- package/dist/engines/pi/models.js +147 -67
- package/dist/engines/pi/open.d.ts +25 -11
- package/dist/engines/pi/open.js +42 -20
- package/dist/engines/pi/report.d.ts +3 -0
- package/dist/engines/pi/report.js +11 -0
- package/dist/engines/pi/session-builder.js +6 -22
- package/dist/engines/pi/session-control.d.ts +4 -4
- package/dist/engines/pi/session-control.js +72 -40
- package/dist/engines/pi/session-settings.d.ts +9 -0
- package/dist/engines/pi/session-settings.js +17 -0
- package/dist/engines/pi/session-store.d.ts +2 -2
- package/dist/engines/pi/tool-context.d.ts +3 -5
- package/dist/engines/pi/tool-context.js +4 -16
- package/dist/engines/pi/tool.d.ts +32 -21
- package/dist/engines/pi/tool.js +21 -25
- package/dist/engines/pi/turn-kit.d.ts +18 -9
- package/dist/engines/pi/turn-kit.js +104 -27
- package/dist/pi.d.ts +1 -1
- package/dist/pi.js +1 -1
- package/dist/session.d.ts +60 -8
- package/dist/session.js +5 -1
- package/package.json +5 -5
- package/dist/engines/pi/search-tools.d.ts +0 -4
- package/dist/engines/pi/search-tools.js +0 -109
|
@@ -17,14 +17,12 @@ export declare function agentSessionManager(session: AgentSession, sessionId: st
|
|
|
17
17
|
* activate deferred tools mid-turn without tool.ts importing the engine. pi anchors the addition in the transcript
|
|
18
18
|
* (a system message carrying `toolsAdded`, written after the batch of tool results the activating call belongs to),
|
|
19
19
|
* so providers with native deferred loading keep their
|
|
20
|
-
* prompt-cache prefix
|
|
21
|
-
* is fastagent's own `fastagent:tool-activation` entry, replayed by the per-invoke restore
|
|
22
|
-
* (agent-session-factory.ts).
|
|
20
|
+
* prompt-cache prefix. The same transcript declarations restore the loadout on the next binding.
|
|
23
21
|
*/
|
|
24
22
|
export interface ToolActivation {
|
|
25
23
|
/** Names of the currently ACTIVE tools. */
|
|
26
24
|
active(): string[];
|
|
27
|
-
/** Every registered tool (active or not)
|
|
25
|
+
/** Every registered tool (active or not). */
|
|
28
26
|
registered(): Array<{
|
|
29
27
|
name: string;
|
|
30
28
|
description: string;
|
|
@@ -37,7 +35,7 @@ export interface ToolActivation {
|
|
|
37
35
|
activate(names: string[]): string[];
|
|
38
36
|
}
|
|
39
37
|
/** The activation bridge over a live pi session — the ONE implementation, for both consumers. */
|
|
40
|
-
export declare function sessionToolActivation(session: AgentSession
|
|
38
|
+
export declare function sessionToolActivation(session: AgentSession): ToolActivation;
|
|
41
39
|
export interface TurnContext {
|
|
42
40
|
/** Working directory for this execution. */
|
|
43
41
|
cwd?: string;
|
|
@@ -16,28 +16,16 @@ export function agentSessionManager(session, sessionId) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
/** The activation bridge over a live pi session — the ONE implementation, for both consumers. */
|
|
19
|
-
export function sessionToolActivation(session
|
|
19
|
+
export function sessionToolActivation(session) {
|
|
20
20
|
return {
|
|
21
21
|
active: () => session.getActiveToolNames(),
|
|
22
22
|
registered: () => session.getAllTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
|
|
23
23
|
activate(names) {
|
|
24
24
|
const current = session.getActiveToolNames();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
session.setActiveToolsByName([...current, ...added]);
|
|
29
|
-
onActivated?.(added);
|
|
30
|
-
return added;
|
|
25
|
+
session.setActiveToolsByName([...current, ...names]);
|
|
26
|
+
const before = new Set(current);
|
|
27
|
+
return session.getActiveToolNames().filter((name) => !before.has(name));
|
|
31
28
|
},
|
|
32
29
|
};
|
|
33
30
|
}
|
|
34
31
|
export const turnContext = new AsyncLocalStorage();
|
|
35
|
-
/**
|
|
36
|
-
* dedupe → keep registered names only (pi's setters THROW on unknown) → exclude already-active → the names to actually
|
|
37
|
-
* add (empty = nothing to set).
|
|
38
|
-
*/
|
|
39
|
-
function additiveActivation(registered, current, names) {
|
|
40
|
-
const known = new Set(registered);
|
|
41
|
-
const active = new Set(current);
|
|
42
|
-
return [...new Set(names)].filter((name) => known.has(name) && !active.has(name));
|
|
43
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ExtensionToolContext, ToolAnnotations, ToolDefinition, ToolExposure, ToolNamespace } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { type ModuleLoadFailure } from "../../loader.ts";
|
|
5
5
|
import { type DeclaredSecret } from "../../declared-secrets.ts";
|
|
@@ -11,10 +11,23 @@ export interface ToolContext {
|
|
|
11
11
|
signal?: AbortSignal;
|
|
12
12
|
sessionManager?: ReadonlySessionManager;
|
|
13
13
|
/**
|
|
14
|
-
* Tool activation for the current turn
|
|
15
|
-
* the built-in `search_tools` is one consumer).
|
|
14
|
+
* Tool activation for the current turn; Pi records changes in the transcript.
|
|
16
15
|
*/
|
|
17
16
|
tools?: ToolActivation;
|
|
17
|
+
/**
|
|
18
|
+
* Run another mounted tool through pi's own pipeline: argument validation, the definition's `tool_call` and
|
|
19
|
+
* `tool_result` hooks, cancellation (this call's signal by default). It reaches the active `direct` tools and every
|
|
20
|
+
* `codemode` or `deferred` one. A tool failure comes back as `isError: true`, never as a rejection. The nested call
|
|
21
|
+
* is observed with `parentToolCallId` and does not enter the transcript. Undefined without a session
|
|
22
|
+
* (`fastagent tool`).
|
|
23
|
+
*/
|
|
24
|
+
executeTool?: ExtensionToolContext["executeTool"];
|
|
25
|
+
/**
|
|
26
|
+
* Report progress while this call runs: a snapshot of everything so far (each call replaces the last), wrapped the
|
|
27
|
+
* way a return value is. Its last line is the call's status (`tool_progress` on the invoke stream); session-control
|
|
28
|
+
* observers receive the whole snapshot. Undefined when the caller takes no progress (`fastagent tool`).
|
|
29
|
+
*/
|
|
30
|
+
onUpdate?: (partial: unknown) => void;
|
|
18
31
|
/** THE values of {@link DefineToolOptions.secrets}, keyed by the names this tool declared — read
|
|
19
32
|
* from the process environment per call, so a value rotated THERE takes effect without a restart
|
|
20
33
|
* (one rotated in `.secrets/.env` does not: that file is loaded once at startup). This is how a
|
|
@@ -26,11 +39,20 @@ export interface DefineToolOptions<I extends z.ZodType, S extends readonly strin
|
|
|
26
39
|
name?: string;
|
|
27
40
|
description: string;
|
|
28
41
|
input: I;
|
|
42
|
+
/** Pi's native visibility: direct (default), model-only, codemode, deferred, or hidden. */
|
|
43
|
+
exposure?: ToolExposure;
|
|
44
|
+
/** Direct tools start active unless false; settings may still select them. */
|
|
45
|
+
defaultActive?: boolean;
|
|
29
46
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
47
|
+
* MCP-style hints about what the tool does (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`).
|
|
48
|
+
* Pi does not enforce them; a permission extension reads them from `pi.getAllTools()` to decide which calls to
|
|
49
|
+
* confirm, and treats a tool without them as possibly destructive.
|
|
32
50
|
*/
|
|
33
|
-
|
|
51
|
+
annotations?: ToolAnnotations;
|
|
52
|
+
/** Groups the tool with related ones (`{ name, description? }`): codemode lists a namespace under one heading. */
|
|
53
|
+
namespace?: ToolNamespace;
|
|
54
|
+
/** Structured output schema for programmatic callers such as codemode. */
|
|
55
|
+
output?: z.ZodType;
|
|
34
56
|
/** pi's per-tool execution mode: "sequential" makes pi run any batch containing this tool serially. */
|
|
35
57
|
executionMode?: "sequential" | "parallel";
|
|
36
58
|
/**
|
|
@@ -46,25 +68,14 @@ export interface DefineToolOptions<I extends z.ZodType, S extends readonly strin
|
|
|
46
68
|
}) => unknown | Promise<unknown>;
|
|
47
69
|
}
|
|
48
70
|
/** AgentTool with Pi's optional per-call context; absent for sessionless CLI execution. */
|
|
49
|
-
export type MountedTool = Omit<AgentTool, "execute"> & {
|
|
50
|
-
execute(...args: [...Parameters<AgentTool["execute"]>, context?:
|
|
71
|
+
export type MountedTool = Omit<AgentTool, "execute"> & Pick<ToolDefinition, "exposure" | "defaultActive" | "annotations" | "namespace"> & {
|
|
72
|
+
execute(...args: [...Parameters<AgentTool["execute"]>, context?: ExtensionToolContext]): ReturnType<AgentTool["execute"]>;
|
|
51
73
|
};
|
|
52
|
-
/**
|
|
53
|
-
export type FastagentTool =
|
|
54
|
-
deferred?: boolean;
|
|
74
|
+
/** A Pi tool with fastagent's declared secrets. */
|
|
75
|
+
export type FastagentTool = MountedTool & {
|
|
55
76
|
/** {@link DefineToolOptions.secrets} — read back by `readSecretDeclaration`; pi ignores it. */
|
|
56
77
|
secrets?: readonly string[];
|
|
57
78
|
};
|
|
58
|
-
/**
|
|
59
|
-
* Read the {@link DefineToolOptions.deferred} marker off a mounted tool (extra property on the AgentTool object — pi
|
|
60
|
-
* ignores it).
|
|
61
|
-
*/
|
|
62
|
-
export declare function isDeferredTool(tool: MountedTool): boolean;
|
|
63
|
-
/**
|
|
64
|
-
* The same tool without the deferred marker — for a loader that must stay active (a deferred loader could never be
|
|
65
|
-
* activated and would strand every deferred tool).
|
|
66
|
-
*/
|
|
67
|
-
export declare function stripDeferredMarker(tool: MountedTool): MountedTool;
|
|
68
79
|
export declare function defineTool<I extends z.ZodType, const S extends readonly string[] = readonly []>(options: DefineToolOptions<I, S>): FastagentTool;
|
|
69
80
|
/** A discarded same-name tool (within `tools/`, or against an existing tool). */
|
|
70
81
|
export interface ToolCollision {
|
package/dist/engines/pi/tool.js
CHANGED
|
@@ -5,27 +5,11 @@ import { z } from "zod";
|
|
|
5
5
|
import { loadModuleDir } from "../../loader.js";
|
|
6
6
|
import { readSecretDeclaration, secretValues } from "../../declared-secrets.js";
|
|
7
7
|
import { turnContext } from "./tool-context.js";
|
|
8
|
-
/**
|
|
9
|
-
* Read the {@link DefineToolOptions.deferred} marker off a mounted tool (extra property on the AgentTool object — pi
|
|
10
|
-
* ignores it).
|
|
11
|
-
*/
|
|
12
|
-
export function isDeferredTool(tool) {
|
|
13
|
-
return tool.deferred === true;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* The same tool without the deferred marker — for a loader that must stay active (a deferred loader could never be
|
|
17
|
-
* activated and would strand every deferred tool).
|
|
18
|
-
*/
|
|
19
|
-
export function stripDeferredMarker(tool) {
|
|
20
|
-
if (!isDeferredTool(tool))
|
|
21
|
-
return tool;
|
|
22
|
-
const { deferred: _drop, ...active } = tool;
|
|
23
|
-
return active;
|
|
24
|
-
}
|
|
25
8
|
/**
|
|
26
9
|
* Ask the provider to constrain sampling to the tool's schema, the posture pi's own built-ins take. `"prefer"`
|
|
27
|
-
* rather than `"require"`: a schema pi cannot express strictly,
|
|
28
|
-
*
|
|
10
|
+
* rather than `"require"`: a schema pi cannot express strictly, one using a keyword the provider's strict mode rejects
|
|
11
|
+
* (Anthropic: numeric bounds, which every `z.number().int()` carries), or a provider without strict mode silently
|
|
12
|
+
* falls back to an ordinary function tool instead of failing the turn.
|
|
29
13
|
*
|
|
30
14
|
* Nothing here has to undo pi's strict rewrite. To express "optional" strictly, pi marks every property required and
|
|
31
15
|
* unions the optional ones with `null`, so a constrained model emits `null` where it would have omitted the key — and
|
|
@@ -36,12 +20,13 @@ export function stripDeferredMarker(tool) {
|
|
|
36
20
|
*/
|
|
37
21
|
const CONSTRAINED_SAMPLING = { type: "json_schema", strict: "prefer" };
|
|
38
22
|
/** Wrap a plain return value into pi's tool-result shape; pass a full result through unchanged. */
|
|
39
|
-
function wrapResult(value) {
|
|
23
|
+
function wrapResult(value, output) {
|
|
40
24
|
if (value && typeof value === "object" && Array.isArray(value.content)) {
|
|
41
25
|
return value;
|
|
42
26
|
}
|
|
27
|
+
const structuredContent = output ? output.parse(value) : undefined;
|
|
43
28
|
const text = typeof value === "string" ? value : JSON.stringify(value ?? null);
|
|
44
|
-
return { content: [{ type: "text", text }], details: value };
|
|
29
|
+
return { content: [{ type: "text", text }], details: value, ...(output ? { structuredContent } : {}) };
|
|
45
30
|
}
|
|
46
31
|
// `S` defaults to the EMPTY tuple, not `readonly string[]`: a tool that declares nothing then gets
|
|
47
32
|
// `ctx.secrets` with no keys, so `ctx.secrets.X_API_KEY` fails to compile instead of type-checking as
|
|
@@ -55,15 +40,23 @@ export function defineTool(options) {
|
|
|
55
40
|
description: options.description,
|
|
56
41
|
parameters,
|
|
57
42
|
constrainedSampling: CONSTRAINED_SAMPLING,
|
|
58
|
-
...(options.
|
|
43
|
+
...(options.exposure ? { exposure: options.exposure } : {}),
|
|
44
|
+
...(options.defaultActive !== undefined ? { defaultActive: options.defaultActive } : {}),
|
|
45
|
+
...(options.annotations ? { annotations: options.annotations } : {}),
|
|
46
|
+
...(options.namespace ? { namespace: options.namespace } : {}),
|
|
47
|
+
...(options.output ? { outputSchema: z.toJSONSchema(options.output) } : {}),
|
|
59
48
|
...(options.executionMode ? { executionMode: options.executionMode } : {}),
|
|
60
49
|
...(options.secrets?.length ? { secrets: options.secrets } : {}),
|
|
61
|
-
async execute(_toolCallId, rawParams, signal) {
|
|
50
|
+
async execute(_toolCallId, rawParams, signal, onUpdate, context) {
|
|
62
51
|
const parsed = options.input.safeParse(rawParams);
|
|
63
52
|
if (!parsed.success) {
|
|
64
53
|
// Validation failure is reported TO THE MODEL (it can correct and retry), not thrown.
|
|
65
54
|
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
66
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: `Invalid arguments: ${detail}` }],
|
|
57
|
+
details: { error: detail },
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
67
60
|
}
|
|
68
61
|
const store = turnContext.getStore();
|
|
69
62
|
return wrapResult(await options.execute(parsed.data, {
|
|
@@ -71,8 +64,11 @@ export function defineTool(options) {
|
|
|
71
64
|
signal,
|
|
72
65
|
sessionManager: store?.sessionManager,
|
|
73
66
|
tools: store?.tools,
|
|
67
|
+
// Per CALL, not per turn: pi binds `executeTool` to this call's id, the parent of what it runs.
|
|
68
|
+
executeTool: context?.executeTool,
|
|
69
|
+
onUpdate: onUpdate && ((partial) => onUpdate(wrapResult(partial))),
|
|
74
70
|
secrets: secretValues(options.secrets),
|
|
75
|
-
}));
|
|
71
|
+
}), options.output);
|
|
76
72
|
},
|
|
77
73
|
};
|
|
78
74
|
return tool;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import type { AssistantMessage, ImageContent } from "@earendil-works/pi-ai";
|
|
1
|
+
import { type AssistantMessage, type ImageContent } from "@earendil-works/pi-ai";
|
|
3
2
|
import { type AgentEvent, type Prompt } from "../../agent.ts";
|
|
4
|
-
import type { SessionEvent } from "../../session.ts";
|
|
3
|
+
import type { AnswerOutcome, PromptDisposition, SessionEvent } from "../../session.ts";
|
|
5
4
|
export type Release = () => void;
|
|
6
5
|
export interface Lease {
|
|
7
6
|
/** Try to acquire exclusive write access for the session (fail-fast). */
|
|
@@ -14,9 +13,15 @@ export declare function classifyRetryable(details: string, signal: {
|
|
|
14
13
|
code?: unknown;
|
|
15
14
|
}): boolean;
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
16
|
+
* How an answer ended, read off the stopReason pi records on the message. The ONE reading: `message_finished`,
|
|
17
|
+
* the `assistant` entry `entries()` publishes, and the invoke terminal all derive from it, so the live outcome and
|
|
18
|
+
* the recorded one cannot disagree.
|
|
19
|
+
*/
|
|
20
|
+
export declare function answerOutcome(message: AssistantMessage): AnswerOutcome | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Terminal mapping, decided by the resolved message's outcome: pi's `prompt()` RESOLVES a message with stopReason
|
|
18
23
|
* "error"/"aborted" rather than throwing, so relying on catch alone would miss that whole failure class and violate
|
|
19
|
-
* SPEC MUST 1.
|
|
24
|
+
* SPEC MUST 1. A truncated final answer still completes the run.
|
|
20
25
|
*/
|
|
21
26
|
export declare function toTerminal(message: AssistantMessage): AgentEvent;
|
|
22
27
|
export declare function errorToTerminal(error: unknown): Extract<AgentEvent, {
|
|
@@ -34,11 +39,15 @@ export declare function toPiPromptOptions(prompt: Prompt, delivery: "prompt" | "
|
|
|
34
39
|
} | undefined>;
|
|
35
40
|
/** Live modulation handles for one active run — what the control plane's `dispatch` routes to. */
|
|
36
41
|
export interface RunControls {
|
|
37
|
-
steer(prompt: Prompt): Promise<
|
|
38
|
-
followUp(prompt: Prompt): Promise<
|
|
42
|
+
steer(prompt: Prompt): Promise<PromptDisposition>;
|
|
43
|
+
followUp(prompt: Prompt): Promise<PromptDisposition>;
|
|
39
44
|
abort(): Promise<void>;
|
|
40
45
|
}
|
|
41
46
|
/** The DATA-plane observation seam: every rich event of every run, pushed as it happens. */
|
|
42
47
|
export type SessionObserver = (session: string, event: SessionEvent, run?: RunControls) => void;
|
|
43
|
-
/**
|
|
44
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The SPEC projection of ONE run's rich stream. Stateful only for `tool_progress`, the one event that is a status
|
|
50
|
+
* rather than a fact: an outer call's status line is sent when it changes, and a call that call makes, at any depth,
|
|
51
|
+
* is its status. Nested calls are otherwise the observation plane's alone.
|
|
52
|
+
*/
|
|
53
|
+
export declare function agentEventProjection(): (se: SessionEvent) => AgentEvent | null;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** The turn mechanism's ENGINE-agnostic half: the parts that describe a turn rather than pi. */
|
|
2
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
|
+
import { contentText } from "@earendil-works/pi-ai";
|
|
1
4
|
import { ABORTED_CODE } from "../../agent.js";
|
|
2
5
|
import { log } from "../../log.js";
|
|
3
6
|
export function inProcessLease() {
|
|
@@ -71,19 +74,38 @@ function messageSignal(message) {
|
|
|
71
74
|
return {};
|
|
72
75
|
}
|
|
73
76
|
/**
|
|
74
|
-
*
|
|
77
|
+
* How an answer ended, read off the stopReason pi records on the message. The ONE reading: `message_finished`,
|
|
78
|
+
* the `assistant` entry `entries()` publishes, and the invoke terminal all derive from it, so the live outcome and
|
|
79
|
+
* the recorded one cannot disagree.
|
|
80
|
+
*/
|
|
81
|
+
export function answerOutcome(message) {
|
|
82
|
+
const error = message.errorMessage ? { error: { message: message.errorMessage } } : {};
|
|
83
|
+
switch (message.stopReason) {
|
|
84
|
+
case "error":
|
|
85
|
+
return { status: "failed", ...error };
|
|
86
|
+
case "aborted":
|
|
87
|
+
return { status: "aborted", ...error };
|
|
88
|
+
case "length":
|
|
89
|
+
return { status: "truncated" };
|
|
90
|
+
default:
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Terminal mapping, decided by the resolved message's outcome: pi's `prompt()` RESOLVES a message with stopReason
|
|
75
96
|
* "error"/"aborted" rather than throwing, so relying on catch alone would miss that whole failure class and violate
|
|
76
|
-
* SPEC MUST 1.
|
|
97
|
+
* SPEC MUST 1. A truncated final answer still completes the run.
|
|
77
98
|
*/
|
|
78
99
|
export function toTerminal(message) {
|
|
79
|
-
|
|
100
|
+
const outcome = answerOutcome(message);
|
|
101
|
+
if (outcome?.status === "aborted") {
|
|
80
102
|
// A deliberate stop (a control-plane or consumer abort), not an error — see {@link ABORTED_CODE} for the consumer
|
|
81
103
|
// contract (design §6).
|
|
82
|
-
const details = message
|
|
104
|
+
const details = outcome.error?.message ?? "run aborted";
|
|
83
105
|
return { type: "failed", details, retryable: false, code: ABORTED_CODE };
|
|
84
106
|
}
|
|
85
|
-
if (
|
|
86
|
-
const details = message
|
|
107
|
+
if (outcome?.status === "failed") {
|
|
108
|
+
const details = outcome.error?.message ?? "model stopped: error";
|
|
87
109
|
return { type: "failed", details, retryable: classifyRetryable(details, messageSignal(message)) };
|
|
88
110
|
}
|
|
89
111
|
return { type: "completed" };
|
|
@@ -119,27 +141,82 @@ export async function toPiPromptOptions(prompt, delivery) {
|
|
|
119
141
|
}));
|
|
120
142
|
return { images };
|
|
121
143
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const d = se.data;
|
|
135
|
-
return { type: "tool_ended", id: d.id, isError: d.isError, content: d.content };
|
|
136
|
-
}
|
|
137
|
-
case "retry_scheduled": {
|
|
138
|
-
// `operation` (compaction | branch_summary) stays session-plane vocabulary.
|
|
139
|
-
const d = se.data;
|
|
140
|
-
return { type: "retrying", attempt: d.attempt, maxAttempts: d.maxAttempts, delayMs: d.delayMs, reason: d.error };
|
|
141
|
-
}
|
|
142
|
-
default:
|
|
144
|
+
/**
|
|
145
|
+
* The SPEC projection of ONE run's rich stream. Stateful only for `tool_progress`, the one event that is a status
|
|
146
|
+
* rather than a fact: an outer call's status line is sent when it changes, and a call that call makes, at any depth,
|
|
147
|
+
* is its status. Nested calls are otherwise the observation plane's alone.
|
|
148
|
+
*/
|
|
149
|
+
export function agentEventProjection() {
|
|
150
|
+
/** Each nested call's outer call, the one the stream announced with `tool_started`. */
|
|
151
|
+
const outerOf = new Map();
|
|
152
|
+
/** The status line each outer call last reported. */
|
|
153
|
+
const shown = new Map();
|
|
154
|
+
const progress = (id, text) => {
|
|
155
|
+
if (text === undefined || shown.get(id) === text)
|
|
143
156
|
return null;
|
|
157
|
+
shown.set(id, text);
|
|
158
|
+
return { type: "tool_progress", id, text };
|
|
159
|
+
};
|
|
160
|
+
return (se) => {
|
|
161
|
+
switch (se.type) {
|
|
162
|
+
case "message_delta": {
|
|
163
|
+
const d = se.data;
|
|
164
|
+
return d.channel === "text" ? { type: "text", delta: d.delta } : { type: "thinking", delta: d.delta };
|
|
165
|
+
}
|
|
166
|
+
case "tool_started": {
|
|
167
|
+
const d = se.data;
|
|
168
|
+
if (!d.parentToolCallId)
|
|
169
|
+
return { type: "tool_started", id: d.id, name: d.name, args: d.args };
|
|
170
|
+
const outer = outerOf.get(d.parentToolCallId) ?? d.parentToolCallId;
|
|
171
|
+
outerOf.set(d.id, outer);
|
|
172
|
+
return progress(outer, callLine(d.name, d.args));
|
|
173
|
+
}
|
|
174
|
+
case "tool_progress": {
|
|
175
|
+
const d = se.data;
|
|
176
|
+
// A nested call's own output is not its caller's status; the nested call starting is (above).
|
|
177
|
+
if (d.parentToolCallId || !Array.isArray(d.partialResult.content))
|
|
178
|
+
return null;
|
|
179
|
+
return progress(d.id, lastLine(contentText(d.partialResult.content, "\n")));
|
|
180
|
+
}
|
|
181
|
+
case "tool_finished": {
|
|
182
|
+
const d = se.data;
|
|
183
|
+
if (d.parentToolCallId)
|
|
184
|
+
return null;
|
|
185
|
+
return { type: "tool_ended", id: d.id, isError: d.isError, content: d.content };
|
|
186
|
+
}
|
|
187
|
+
case "retry_scheduled": {
|
|
188
|
+
// `operation` (compaction | branch_summary) stays session-plane vocabulary.
|
|
189
|
+
const d = se.data;
|
|
190
|
+
return {
|
|
191
|
+
type: "retrying",
|
|
192
|
+
attempt: d.attempt,
|
|
193
|
+
maxAttempts: d.maxAttempts,
|
|
194
|
+
delayMs: d.delayMs,
|
|
195
|
+
reason: d.error,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
default:
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The line a terminal shows last: control sequences removed, and a bare `\r`, a progress bar redrawing in place, ends a
|
|
205
|
+
* line like `\n` does. Undefined when there is no visible text yet (a shell that has printed nothing).
|
|
206
|
+
*/
|
|
207
|
+
function lastLine(text) {
|
|
208
|
+
const lines = stripVTControlCharacters(text).split(/\r\n|\r|\n/);
|
|
209
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
210
|
+
const line = lines[i]?.trim();
|
|
211
|
+
if (line)
|
|
212
|
+
return line;
|
|
144
213
|
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
/** A nested call as its caller's status: the tool and its first plain argument, on one line (`weather London`). */
|
|
217
|
+
function callLine(name, args) {
|
|
218
|
+
const first = args !== null && typeof args === "object" && !Array.isArray(args)
|
|
219
|
+
? Object.values(args).find((value) => typeof value === "string" || typeof value === "number")
|
|
220
|
+
: undefined;
|
|
221
|
+
return first === undefined ? name : `${name} ${String(first).replace(/\s+/g, " ").trim()}`.trim();
|
|
145
222
|
}
|
package/dist/pi.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { z } from "zod";
|
|
|
5
5
|
export type { AgentTool, ExecutionEnv, Skill, SkillDiagnostic } from "@earendil-works/pi-agent-core";
|
|
6
6
|
/** A conversation record, as the tool runtime and the control plane hold it. */
|
|
7
7
|
export type { SessionManager, SessionEntry as PiSessionEntry } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
export { availableModelsFromDir, createPiAgentFromDir, type CreatePiAgentFromDirOptions, refreshModelCatalog, } from "./engines/pi/open.ts";
|
|
8
|
+
export { availableModelsFromDir, createPiAgentFromDir, type CreatePiAgentFromDirOptions, refreshMachineModelCatalog, refreshModelCatalog, } from "./engines/pi/open.ts";
|
|
9
9
|
export type { LoadedDefinition, SkillCollision } from "./engines/pi/definition.ts";
|
|
10
10
|
export { defineConfig, listModels, resolveModel, type FastagentConfig, } from "./engines/pi/config.ts";
|
|
11
11
|
export type { SessionObserver } from "./engines/pi/turn-kit.ts";
|
package/dist/pi.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export { createPiAgent, createPiAgentFromDefinition, } from "./engines/pi/create.js";
|
|
3
3
|
export { defineTool, } from "./engines/pi/tool.js";
|
|
4
4
|
export { z } from "zod";
|
|
5
|
-
export { availableModelsFromDir, createPiAgentFromDir, refreshModelCatalog, } from "./engines/pi/open.js";
|
|
5
|
+
export { availableModelsFromDir, createPiAgentFromDir, refreshMachineModelCatalog, refreshModelCatalog, } from "./engines/pi/open.js";
|
|
6
6
|
export { defineConfig, listModels, resolveModel, } from "./engines/pi/config.js";
|
|
7
7
|
export { inProcessLease } from "./engines/pi/turn-kit.js";
|
|
8
8
|
export { createPiSessionControl, } from "./engines/pi/session-control.js";
|
package/dist/session.d.ts
CHANGED
|
@@ -54,14 +54,22 @@ export interface Session {
|
|
|
54
54
|
*/
|
|
55
55
|
image(ref: string): Promise<ImageRef | undefined>;
|
|
56
56
|
events(): SessionEventStream;
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Set durable session properties, applied by the session's next turn. An id with no record (nothing has written it
|
|
59
|
+
* yet) takes them too: the write creates its record, and the session's first turn runs on them. Refused there:
|
|
60
|
+
* `leafEntryId` (no entries to point at) and an id no client could address ({@link isAddressableSession}).
|
|
61
|
+
*/
|
|
58
62
|
update(patch: SessionUpdate): Promise<SessionResult>;
|
|
59
63
|
/**
|
|
60
64
|
* Join the active run: delivered after the current turn's tool calls, before the next model call. `prompt.text`
|
|
61
|
-
* must not be empty (`invalid_command`): a queued prompt is reported by its text ({@link PendingPrompts}).
|
|
65
|
+
* must not be empty (`invalid_command`): a queued prompt is reported by its text ({@link PendingPrompts}). An
|
|
66
|
+
* accepted result carries its {@link PromptDisposition}.
|
|
62
67
|
*/
|
|
63
68
|
steer(prompt: Prompt): Promise<SessionResult>;
|
|
64
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Queue for the active run, FIFO, delivered when it is otherwise idle. Same non-empty `text` rule and
|
|
71
|
+
* {@link PromptDisposition} as `steer`.
|
|
72
|
+
*/
|
|
65
73
|
followUp(prompt: Prompt): Promise<SessionResult>;
|
|
66
74
|
/** Stop the active run — its queues, its retry delay, its cancellable tool work. */
|
|
67
75
|
abort(): Promise<SessionResult>;
|
|
@@ -121,11 +129,22 @@ export interface SessionCapabilities {
|
|
|
121
129
|
delete: boolean;
|
|
122
130
|
/** Which {@link SessionUpdate} fields this deployment accepts. */
|
|
123
131
|
updatable: SessionUpdateField[];
|
|
124
|
-
/** The
|
|
125
|
-
allowedModels?:
|
|
132
|
+
/** The models `update({ model })` accepts, by `spec` — present iff `model` is updatable. */
|
|
133
|
+
allowedModels?: ModelDescriptor[];
|
|
126
134
|
toolProgress: boolean;
|
|
127
135
|
usage: boolean;
|
|
128
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* A model as a picker shows it, before any session runs on it. `thinkingLevels` is what `update({ thinkingLevel })`
|
|
139
|
+
* accepts for a session on this model; `name` and `contextWindow` are absent when the model does not declare them.
|
|
140
|
+
*/
|
|
141
|
+
export interface ModelDescriptor {
|
|
142
|
+
/** What `update({ model })` takes. */
|
|
143
|
+
spec: string;
|
|
144
|
+
name?: string;
|
|
145
|
+
thinkingLevels: string[];
|
|
146
|
+
contextWindow?: number;
|
|
147
|
+
}
|
|
129
148
|
/** One name a client can offer the user. */
|
|
130
149
|
export interface AgentCommand {
|
|
131
150
|
name: string;
|
|
@@ -144,7 +163,11 @@ export declare const UNSUPPORTED_CAPABILITY_CODE = "unsupported_capability";
|
|
|
144
163
|
export declare const NO_ACTIVE_RUN_CODE = "no_active_run";
|
|
145
164
|
/** Stable `SessionResult.error.code` for a PAYLOAD that is invalid for this runtime. */
|
|
146
165
|
export declare const INVALID_COMMAND_CODE = "invalid_command";
|
|
147
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Stable `SessionResult.error.code` for a write that needs a record (fork's source, compact, delete) against an id
|
|
168
|
+
* that has none, because nothing has written it yet. `update()` creates the record instead, and answers this code only
|
|
169
|
+
* when a concurrent `delete()` removes the record between its read and its write.
|
|
170
|
+
*/
|
|
148
171
|
export declare const NO_SUCH_SESSION_CODE = "no_such_session";
|
|
149
172
|
/** Stable `SessionResult.error.code` for a write rejected BEFORE acceptance with nothing durable landed. */
|
|
150
173
|
export declare const BOUNDARY_COMMAND_FAILED_CODE = "boundary_command_failed";
|
|
@@ -180,6 +203,7 @@ export declare const PARTIAL_UPDATE_CODE = "partial_update";
|
|
|
180
203
|
export type SessionResult = {
|
|
181
204
|
ok: true;
|
|
182
205
|
runId?: string;
|
|
206
|
+
disposition?: PromptDisposition;
|
|
183
207
|
} | {
|
|
184
208
|
ok: false;
|
|
185
209
|
error: {
|
|
@@ -188,6 +212,13 @@ export type SessionResult = {
|
|
|
188
212
|
retryable: boolean;
|
|
189
213
|
};
|
|
190
214
|
};
|
|
215
|
+
/**
|
|
216
|
+
* What became of an accepted `steer`/`followUp` prompt — present on exactly those results. `queued`: it waits in
|
|
217
|
+
* {@link PendingPrompts} until the run takes it in. `handled`: the engine consumed it before the queue (in pi, a
|
|
218
|
+
* definition extension's `input` handler), so it never enters the conversation, and no `queue_changed` or
|
|
219
|
+
* `user_message` reports it.
|
|
220
|
+
*/
|
|
221
|
+
export type PromptDisposition = "queued" | "handled";
|
|
191
222
|
/** One session in {@link SessionControl.sessions} — a conversation-list row, not a session's contents. */
|
|
192
223
|
export interface SessionSummary {
|
|
193
224
|
session: string;
|
|
@@ -205,7 +236,10 @@ export interface SessionState {
|
|
|
205
236
|
/** `compacting` refers to MANUAL compaction (`compact`) at a session boundary. */
|
|
206
237
|
status: "idle" | "running" | "compacting";
|
|
207
238
|
activeRunId?: string;
|
|
208
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* What this session will RUN with, not what was recorded. An id with no record reports what its first turn would run
|
|
241
|
+
* on: the defaults, the level clamped to the model.
|
|
242
|
+
*/
|
|
209
243
|
model?: string;
|
|
210
244
|
thinkingLevel?: string;
|
|
211
245
|
/** What `update({ thinkingLevel })` accepts for THIS session — re-read after a model change. */
|
|
@@ -319,13 +353,29 @@ export type MessageDeltaEvent = SessionEvent<"message_delta", {
|
|
|
319
353
|
}> & {
|
|
320
354
|
runId: string;
|
|
321
355
|
};
|
|
322
|
-
|
|
356
|
+
/**
|
|
357
|
+
* How an answer ended, when it did not end normally: absent on an answer that did. One value, carried live by its
|
|
358
|
+
* `message_finished` and durably by its `assistant` entry, so a reopened conversation reads what a watcher saw.
|
|
359
|
+
* `truncated` is an answer cut off at the output limit — a property of the answer, not of its run, which may still
|
|
360
|
+
* complete. An answer whose entry a `context_edit { omitted }` targets (`failed` or `truncated`) is an attempt the
|
|
361
|
+
* engine abandoned; a retry usually, but not always, follows it.
|
|
362
|
+
*/
|
|
363
|
+
export type AnswerOutcome = {
|
|
364
|
+
status: "failed" | "aborted" | "truncated";
|
|
365
|
+
error?: {
|
|
366
|
+
message: string;
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
export type MessageFinishedEvent = SessionEvent<"message_finished", {
|
|
370
|
+
outcome?: AnswerOutcome;
|
|
371
|
+
}> & {
|
|
323
372
|
runId: string;
|
|
324
373
|
};
|
|
325
374
|
export type ToolStartedEvent = SessionEvent<"tool_started", {
|
|
326
375
|
id: string;
|
|
327
376
|
name: string;
|
|
328
377
|
args: Json;
|
|
378
|
+
parentToolCallId?: string;
|
|
329
379
|
}> & {
|
|
330
380
|
runId: string;
|
|
331
381
|
};
|
|
@@ -334,6 +384,7 @@ export type ToolProgressEvent = SessionEvent<"tool_progress", {
|
|
|
334
384
|
id: string;
|
|
335
385
|
name: string;
|
|
336
386
|
partialResult: Json;
|
|
387
|
+
parentToolCallId?: string;
|
|
337
388
|
}> & {
|
|
338
389
|
runId: string;
|
|
339
390
|
};
|
|
@@ -341,6 +392,7 @@ export type ToolFinishedEvent = SessionEvent<"tool_finished", {
|
|
|
341
392
|
id: string;
|
|
342
393
|
isError: boolean;
|
|
343
394
|
content: Json;
|
|
395
|
+
parentToolCallId?: string;
|
|
344
396
|
}> & {
|
|
345
397
|
runId: string;
|
|
346
398
|
};
|
package/dist/session.js
CHANGED
|
@@ -28,7 +28,11 @@ export const UNSUPPORTED_CAPABILITY_CODE = "unsupported_capability";
|
|
|
28
28
|
export const NO_ACTIVE_RUN_CODE = "no_active_run";
|
|
29
29
|
/** Stable `SessionResult.error.code` for a PAYLOAD that is invalid for this runtime. */
|
|
30
30
|
export const INVALID_COMMAND_CODE = "invalid_command";
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Stable `SessionResult.error.code` for a write that needs a record (fork's source, compact, delete) against an id
|
|
33
|
+
* that has none, because nothing has written it yet. `update()` creates the record instead, and answers this code only
|
|
34
|
+
* when a concurrent `delete()` removes the record between its read and its write.
|
|
35
|
+
*/
|
|
32
36
|
export const NO_SUCH_SESSION_CODE = "no_such_session";
|
|
33
37
|
/** Stable `SessionResult.error.code` for a write rejected BEFORE acceptance with nothing durable landed. */
|
|
34
38
|
export const BOUNDARY_COMMAND_FAILED_CODE = "boundary_command_failed";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastagent-sh/fastagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
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",
|
|
@@ -101,16 +101,16 @@
|
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
103
|
"@clack/prompts": "^1.6.0",
|
|
104
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
105
|
-
"@earendil-works/pi-ai": "^0.
|
|
106
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
104
|
+
"@earendil-works/pi-agent-core": "^0.99.2",
|
|
105
|
+
"@earendil-works/pi-ai": "^0.99.2",
|
|
106
|
+
"@earendil-works/pi-coding-agent": "^0.99.2",
|
|
107
107
|
"@hono/node-server": "^2.1.1",
|
|
108
108
|
"@larksuiteoapi/node-sdk": "^1.71.1",
|
|
109
109
|
"@smithy/signature-v4": "^5.7.4",
|
|
110
110
|
"chokidar": "^5.0.0",
|
|
111
111
|
"commander": "^15.0.0",
|
|
112
112
|
"croner": "^10.0.1",
|
|
113
|
-
"effect": "4.0.0-rc.
|
|
113
|
+
"effect": "4.0.0-rc.118",
|
|
114
114
|
"giget": "^3.3.0",
|
|
115
115
|
"ignore": "^7.0.5",
|
|
116
116
|
"proper-lockfile": "^4.1.2",
|