@fastagent-sh/fastagent 0.16.0 → 0.16.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 +1 -1
- package/dist/bind.d.ts +34 -0
- package/dist/bind.js +74 -0
- package/dist/channels/agentcore-state.js +21 -6
- package/dist/channels/control.js +3 -0
- package/dist/cli/commands/attach.d.ts +11 -1
- package/dist/cli/commands/attach.js +30 -2
- package/dist/cli/commands/deploy.d.ts +13 -0
- package/dist/cli/commands/deploy.js +18 -6
- package/dist/cli/commands/dev.d.ts +1 -0
- package/dist/cli/commands/dev.js +15 -4
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/logs.d.ts +6 -0
- package/dist/cli/commands/logs.js +27 -0
- package/dist/cli/commands/start.d.ts +1 -0
- package/dist/cli/commands/start.js +11 -3
- package/dist/cli/commands/tool.js +10 -2
- package/dist/cli/program.js +35 -0
- package/dist/cli/serve.d.ts +26 -2
- package/dist/cli/serve.js +71 -17
- package/dist/cli/shared.d.ts +6 -0
- package/dist/cli/shared.js +14 -0
- package/dist/deploy/agentcore/logs.d.ts +30 -0
- package/dist/deploy/agentcore/logs.js +115 -0
- package/dist/deploy/agentcore/plan.d.ts +23 -0
- package/dist/deploy/agentcore/plan.js +41 -3
- package/dist/deploy/container.js +6 -3
- package/dist/deploy/preflight.js +18 -0
- package/dist/engines/pi/config.d.ts +7 -3
- package/dist/engines/pi/config.js +9 -3
- package/dist/engines/pi/create.d.ts +31 -18
- package/dist/engines/pi/create.js +46 -17
- package/dist/engines/pi/harness.d.ts +31 -33
- package/dist/engines/pi/harness.js +24 -76
- package/dist/engines/pi/open.d.ts +2 -2
- package/dist/engines/pi/open.js +2 -1
- package/dist/engines/pi/read-image.d.ts +4 -0
- package/dist/engines/pi/read-image.js +62 -0
- package/dist/engines/pi/search-tools.d.ts +6 -4
- package/dist/engines/pi/search-tools.js +3 -1
- package/dist/engines/pi/session-builder.js +7 -2
- package/dist/engines/pi/session-control.d.ts +12 -3
- package/dist/engines/pi/session-control.js +156 -27
- package/dist/engines/pi/session-settings.d.ts +51 -0
- package/dist/engines/pi/session-settings.js +73 -0
- package/dist/engines/pi/sessions.d.ts +21 -7
- package/dist/engines/pi/sessions.js +43 -0
- package/dist/engines/pi/tool.d.ts +13 -5
- package/dist/engines/pi/wake-tool.d.ts +3 -3
- package/dist/host/node.d.ts +2 -0
- package/dist/host/node.js +2 -1
- package/dist/pi.d.ts +1 -1
- package/dist/session.d.ts +34 -9
- package/package.json +4 -4
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type ExecutionEnv, type Skill, type ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Models, Provider } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { Agent } from "../../agent.ts";
|
|
4
4
|
import { type FastagentConfig } from "./config.ts";
|
|
5
5
|
import { type LoadedDefinition } from "./definition.ts";
|
|
6
|
-
import { piHarnessFactory } from "./harness.ts";
|
|
6
|
+
import { type AnyModel, piHarnessFactory } from "./harness.ts";
|
|
7
7
|
import { type PiSessionStore } from "./sessions.ts";
|
|
8
8
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
9
|
-
import { type
|
|
9
|
+
import { type ToolCollision, type MountedTool } from "./tool.ts";
|
|
10
10
|
import { type Lease, type SessionObserver } from "./invoke.ts";
|
|
11
|
-
/** pi's core default toolset (read/bash/edit/write)
|
|
12
|
-
|
|
11
|
+
/** pi's core default toolset (read/bash/edit/write). Rooted at the ExecutionEnv's cwd, supplied per
|
|
12
|
+
* turn as the harness tool context — hence no argument here. */
|
|
13
|
+
export declare function piDefaultTools(): MountedTool[];
|
|
13
14
|
/** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
|
|
14
|
-
export declare function resolveTools(config: FastagentConfig
|
|
15
|
+
export declare function resolveTools(config: FastagentConfig): MountedTool[];
|
|
15
16
|
/**
|
|
16
17
|
* The full tool set an agent mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
|
|
17
18
|
* existing win), plus the non-default tool names and collisions to report. One source for the
|
|
18
19
|
* dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
|
|
19
20
|
*/
|
|
20
|
-
export declare function resolveAgentTools(config: FastagentConfig, agentDir: string
|
|
21
|
-
tools:
|
|
21
|
+
export declare function resolveAgentTools(config: FastagentConfig, agentDir: string): Promise<{
|
|
22
|
+
tools: MountedTool[];
|
|
22
23
|
toolNames: string[];
|
|
23
24
|
/** Tools registered but not initially active (defineTool `deferred: true`) — discovered/activated
|
|
24
25
|
* via the built-in `search_tools` loader. Surfaced so the operator can see deferral took effect. */
|
|
@@ -33,7 +34,7 @@ export declare function resolveAgentTools(config: FastagentConfig, agentDir: str
|
|
|
33
34
|
* `persona` (from persona.md) replaces the default identity line, keeping the tools list + guidelines.
|
|
34
35
|
*/
|
|
35
36
|
export declare function piBasePrompt(options?: {
|
|
36
|
-
tools?:
|
|
37
|
+
tools?: MountedTool[];
|
|
37
38
|
persona?: string;
|
|
38
39
|
}): string;
|
|
39
40
|
export interface AssembleSystemPromptOptions {
|
|
@@ -63,6 +64,11 @@ type OnAssembly = (parts: {
|
|
|
63
64
|
models: Models;
|
|
64
65
|
harnessFactory: ReturnType<typeof piHarnessFactory>;
|
|
65
66
|
lease: Lease;
|
|
67
|
+
/** The resolved configured pair — what a session without overrides runs on. */
|
|
68
|
+
defaults: {
|
|
69
|
+
model: AnyModel;
|
|
70
|
+
thinkingLevel: ThinkingLevel;
|
|
71
|
+
};
|
|
66
72
|
}) => void;
|
|
67
73
|
/** L1 options. Tier 1: model (spec) + instructions + tools. Tier 2: the injectable ports. */
|
|
68
74
|
export interface CreatePiAgentOptions {
|
|
@@ -76,8 +82,10 @@ export interface CreatePiAgentOptions {
|
|
|
76
82
|
* or a factory re-evaluated per invoke. When {@link skills} are mounted their listing is appended.
|
|
77
83
|
*/
|
|
78
84
|
instructions?: string | (() => string);
|
|
79
|
-
/** `FastagentTool`
|
|
80
|
-
|
|
85
|
+
/** The tool set to mount. `FastagentTool` (AgentTool plus the optional `deferred` marker, see
|
|
86
|
+
* {@link DefineToolOptions}) widens into {@link MountedTool}, which additionally admits pi's default
|
|
87
|
+
* coding tools — they read the turn's ExecutionEnv as a fifth `execute` parameter. */
|
|
88
|
+
tools?: MountedTool[];
|
|
81
89
|
skills?: Skill[];
|
|
82
90
|
/**
|
|
83
91
|
* Extra providers registered on top of the built-ins — your own gateway / self-hosted endpoint /
|
|
@@ -93,8 +101,10 @@ export interface CreatePiAgentOptions {
|
|
|
93
101
|
authPath?: string;
|
|
94
102
|
/** Session persistence. Defaults to in-memory; inject jsonlSessionStore for restart-surviving continuity. */
|
|
95
103
|
sessions?: PiSessionStore;
|
|
96
|
-
/**
|
|
97
|
-
*
|
|
104
|
+
/** Filesystem/process environment. Defaults to a local NodeExecutionEnv at `process.cwd()`, and its
|
|
105
|
+
* cwd is the agent's. The default coding tools (read/bash/edit/write) take it as the turn's tool
|
|
106
|
+
* context, so injecting a constrained one narrows where the agent reads, writes and shells. It does
|
|
107
|
+
* NOT constrain author-written `tools/`, which are code and can import anything. */
|
|
98
108
|
env?: ExecutionEnv;
|
|
99
109
|
/** Single-writer lease. Defaults to in-process fail-fast inProcessLease(). */
|
|
100
110
|
lease?: Lease;
|
|
@@ -116,9 +126,9 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
116
126
|
/** Override the engine base prompt (segment ①). Defaults to piBasePrompt({ tools, persona }) using the
|
|
117
127
|
* live-read persona.md; pass base to fully opt out of persona.md. */
|
|
118
128
|
base?: string;
|
|
119
|
-
/** Override tools. Defaults to piDefaultTools (lock down with a custom list).
|
|
120
|
-
* AgentTool plus the optional `deferred` marker. */
|
|
121
|
-
tools?:
|
|
129
|
+
/** Override tools. Defaults to {@link piDefaultTools} (lock down with a custom list). An authored
|
|
130
|
+
* `FastagentTool[]` (AgentTool plus the optional `deferred` marker) widens into {@link MountedTool}. */
|
|
131
|
+
tools?: MountedTool[];
|
|
122
132
|
/**
|
|
123
133
|
* The agent's working directory: where the default tools operate AND whose ancestors are walked for
|
|
124
134
|
* ② project context (AGENTS.md). Defaults to `dir`. Set it to the enclosing repo so a coding agent
|
|
@@ -135,8 +145,11 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
135
145
|
*/
|
|
136
146
|
authPath?: string;
|
|
137
147
|
sessions?: PiSessionStore;
|
|
138
|
-
/**
|
|
139
|
-
*
|
|
148
|
+
/** Filesystem/process environment; see {@link CreatePiAgentOptions.env}. At THIS rung it does more
|
|
149
|
+
* than root the default tools: persona.md and skills/ are read through it too. Two surfaces stay
|
|
150
|
+
* OUTSIDE it — ② project context (pi's loadProjectContextFiles uses node fs directly; see
|
|
151
|
+
* definition.ts) and author-written `tools/`, which are code and can import anything. Injecting an
|
|
152
|
+
* env narrows the blast radius rather than closing it. */
|
|
140
153
|
env?: ExecutionEnv;
|
|
141
154
|
lease?: Lease;
|
|
142
155
|
/** Observation-plane tap; see {@link CreatePiAgentOptions.observer}. */
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
* they come from the definition; the openers own model/tools — from config resolution).
|
|
12
12
|
*/
|
|
13
13
|
import { formatSkillsForSystemPrompt } from "@earendil-works/pi-agent-core";
|
|
14
|
+
import { createBashTool, createEditTool, createReadTool, createWriteTool, } from "@earendil-works/pi-agent-core";
|
|
14
15
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
15
|
-
import {
|
|
16
|
+
import { readImageProcessor } from "./read-image.js";
|
|
16
17
|
import { defaultAuthPath, resolveModel } from "./config.js";
|
|
17
18
|
import { resolveSecretsDir } from "../../paths.js";
|
|
18
19
|
import { loadAgentDefinition } from "./definition.js";
|
|
19
|
-
import { piHarnessFactory } from "./harness.js";
|
|
20
|
+
import { DEFAULT_THINKING_LEVEL, piHarnessFactory } from "./harness.js";
|
|
20
21
|
import { createPiModels } from "./models.js";
|
|
21
22
|
import { reportDefinitionWarnings } from "./report.js";
|
|
22
23
|
import { inMemorySessionStore } from "./sessions.js";
|
|
@@ -26,15 +27,36 @@ import { createPiAgentFromHarness, inProcessLease } from "./invoke.js";
|
|
|
26
27
|
// ── §1 tools ─────────────────────────────────────────────────────────────────
|
|
27
28
|
//
|
|
28
29
|
// The full pi toolset is the default for fidelity: authors vibe in local pi with it, so serving with
|
|
29
|
-
// fewer tools is behavior drift.
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
// fewer tools is behavior drift. Locking down for public exposure = passing a restricted `tools` list
|
|
31
|
+
// (a deployment posture).
|
|
32
|
+
//
|
|
33
|
+
// These are pi-agent-core's tools, which reach the filesystem and the shell through the
|
|
34
|
+
// {@link ExecutionEnv} the harness hands them per turn — NOT pi-coding-agent's, which are the same four
|
|
35
|
+
// tools wired to `node:fs` directly. Going through the env is the point, and the whole of it: it makes
|
|
36
|
+
// {@link CreatePiAgentOptions.env} the one seam a sandbox adapter has to implement, instead of a knob
|
|
37
|
+
// that governed everything except the tools that actually touch the machine. (It buys no decoupling
|
|
38
|
+
// from pi-coding-agent — definition.ts, models.ts and read-image.ts all import it regardless.)
|
|
39
|
+
//
|
|
40
|
+
// The swap holds only while the two behave alike, which they do NOT for free: core's `read` does
|
|
41
|
+
// nothing with images unless a processor is injected (read-image.ts), and both families are compared
|
|
42
|
+
// on every path in test/tools-parity.test.ts.
|
|
43
|
+
//
|
|
44
|
+
// `chat` is unaffected: it takes these NAMES only and lets pi's own runtime rebuild the tools it
|
|
45
|
+
// renders (see session-builder.ts).
|
|
46
|
+
/** pi's core default toolset (read/bash/edit/write). Rooted at the ExecutionEnv's cwd, supplied per
|
|
47
|
+
* turn as the harness tool context — hence no argument here. */
|
|
48
|
+
export function piDefaultTools() {
|
|
49
|
+
// `read` needs its image pipeline INJECTED (core ships none); see read-image.ts for what is at stake.
|
|
50
|
+
return [
|
|
51
|
+
createReadTool({ imageProcessor: readImageProcessor }),
|
|
52
|
+
createBashTool(),
|
|
53
|
+
createEditTool(),
|
|
54
|
+
createWriteTool(),
|
|
55
|
+
];
|
|
34
56
|
}
|
|
35
57
|
/** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
|
|
36
|
-
export function resolveTools(config
|
|
37
|
-
const defaults = piDefaultTools(
|
|
58
|
+
export function resolveTools(config) {
|
|
59
|
+
const defaults = piDefaultTools();
|
|
38
60
|
return config.tools ? [...defaults, ...config.tools] : defaults;
|
|
39
61
|
}
|
|
40
62
|
/**
|
|
@@ -42,11 +64,12 @@ export function resolveTools(config, cwd) {
|
|
|
42
64
|
* existing win), plus the non-default tool names and collisions to report. One source for the
|
|
43
65
|
* dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
|
|
44
66
|
*/
|
|
45
|
-
export async function resolveAgentTools(config, agentDir
|
|
46
|
-
//
|
|
47
|
-
//
|
|
67
|
+
export async function resolveAgentTools(config, agentDir) {
|
|
68
|
+
// Discovered `tools/` come from `agentDir` (the agent's own surface); the default coding tools carry
|
|
69
|
+
// no root of their own — they operate through the ExecutionEnv handed to them per turn, whose cwd is
|
|
70
|
+
// the workspace.
|
|
48
71
|
const discovered = await loadTools(agentDir);
|
|
49
|
-
const merged = mergeDiscoveredTools(resolveTools(config
|
|
72
|
+
const merged = mergeDiscoveredTools(resolveTools(config), discovered.tools);
|
|
50
73
|
// The built-in `search_tools` loader mounts here — the one place the agent's full tool set is
|
|
51
74
|
// computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; an
|
|
52
75
|
// agent-defined search_tools wins).
|
|
@@ -59,7 +82,7 @@ export async function resolveAgentTools(config, agentDir, cwd = agentDir) {
|
|
|
59
82
|
// defaults, the builtin loader (like wake, a builtin gets its own report line, not an anonymous
|
|
60
83
|
// slot in the author's list — an author-DEFINED search_tools still shows), and deferred tools —
|
|
61
84
|
// each name lives in exactly ONE report slot, and deferred names live in `deferredToolNames`.
|
|
62
|
-
const defaultNames = new Set(piDefaultTools(
|
|
85
|
+
const defaultNames = new Set(piDefaultTools().map((t) => t.name));
|
|
63
86
|
const toolNames = tools
|
|
64
87
|
.filter((t) => !defaultNames.has(t.name) && !isDeferredTool(t) && !(builtinLoaderMounted && t.name === "search_tools"))
|
|
65
88
|
.map((t) => t.name);
|
|
@@ -146,18 +169,24 @@ function buildPiAgent(opts) {
|
|
|
146
169
|
// Materialized here (not defaulted inside createPiAgentFromHarness) so the exposed parts carry
|
|
147
170
|
// the SAME lease instance the agent runs under — boundary mutations must contend on it.
|
|
148
171
|
const lease = opts.lease ?? inProcessLease();
|
|
172
|
+
// The assembly's configured PAIR — handed to the factory and to the control plane as ONE value, so
|
|
173
|
+
// there is no wiring in which they could disagree (which levels exist depends on the model).
|
|
174
|
+
const defaults = {
|
|
175
|
+
model: resolveModel(models, opts.model),
|
|
176
|
+
thinkingLevel: opts.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
177
|
+
};
|
|
149
178
|
const harnessFactory = piHarnessFactory({
|
|
150
179
|
sessions: opts.sessions ?? inMemorySessionStore(),
|
|
151
180
|
env,
|
|
152
181
|
models,
|
|
153
|
-
model:
|
|
182
|
+
model: defaults.model,
|
|
154
183
|
thinkingLevel: opts.thinkingLevel,
|
|
155
184
|
systemPrompt: opts.systemPrompt,
|
|
156
185
|
tools: opts.tools,
|
|
157
186
|
skills: opts.skills,
|
|
158
187
|
live: opts.live,
|
|
159
188
|
});
|
|
160
|
-
opts.onAssembly?.({ models, harnessFactory, lease });
|
|
189
|
+
opts.onAssembly?.({ models, harnessFactory, lease, defaults });
|
|
161
190
|
return createPiAgentFromHarness({ lease, observer: opts.observer, cwd: env.cwd, harnessFactory });
|
|
162
191
|
}
|
|
163
192
|
/**
|
|
@@ -216,7 +245,7 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
216
245
|
let reportedFindings = findingsSignature(definition);
|
|
217
246
|
// Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
|
|
218
247
|
// it; a caller's own search_tools wins).
|
|
219
|
-
const tools = withSearchTool(options.tools ?? piDefaultTools(
|
|
248
|
+
const tools = withSearchTool(options.tools ?? piDefaultTools());
|
|
220
249
|
const agent = buildPiAgent({
|
|
221
250
|
model: options.model,
|
|
222
251
|
thinkingLevel: options.thinkingLevel,
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* historical entries back into context via buildContext().
|
|
8
8
|
*/
|
|
9
9
|
import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
10
|
-
import type {
|
|
10
|
+
import type { ExecutionEnv, ExecutionToolContext, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
11
11
|
import type { Model, Models } from "@earendil-works/pi-ai";
|
|
12
|
-
import type
|
|
12
|
+
import { type PiSessionStore } from "./sessions.ts";
|
|
13
|
+
import { type MountedTool } from "./tool.ts";
|
|
14
|
+
import { type OverrideEntryLike } from "./session-settings.ts";
|
|
13
15
|
/**
|
|
14
16
|
* The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
|
|
15
17
|
* tools a loader activated in that call. The DEDICATED record the resolve below reads: pi's own
|
|
@@ -19,18 +21,30 @@ import type { PiSessionStore } from "./sessions.ts";
|
|
|
19
21
|
* carry only what was actually discovered.
|
|
20
22
|
*/
|
|
21
23
|
export declare const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
|
|
24
|
+
/** The session a factory-built harness is bound to — the seam the activation bridge (invoke.ts) uses
|
|
25
|
+
* to write {@link TOOL_ACTIVATION_ENTRY} deltas (pi's harness keeps its session private). Absent for
|
|
26
|
+
* a harness built outside {@link piHarnessFactory}: activation still works in-turn there, but is not
|
|
27
|
+
* recorded — the factory owns persistence. */
|
|
28
|
+
type AnyHarness = AgentHarness<any>;
|
|
22
29
|
export type PiSession = Awaited<ReturnType<PiSessionStore["openOrCreate"]>>;
|
|
23
|
-
export declare function harnessSession(harness:
|
|
30
|
+
export declare function harnessSession(harness: AnyHarness): PiSession | undefined;
|
|
24
31
|
/**
|
|
25
32
|
* pi's Model with the API-shape generic erased — fastagent only passes models through to the
|
|
26
33
|
* harness, so the generic carries no information. One alias keeps the `any` auditable.
|
|
27
34
|
*/
|
|
28
35
|
export type AnyModel = Model<any>;
|
|
29
36
|
/** Builds a pi harness bound to the given session — called once per invoke. */
|
|
30
|
-
|
|
37
|
+
/** The harness fastagent builds: context-typed on {@link ExecutionToolContext}, because that is what
|
|
38
|
+
* pi's env-backed default tools read (pi 0.83). Custom tools are context-FREE and stay assignable — a
|
|
39
|
+
* four-parameter `execute` satisfies the five-parameter one, so `defineTool` is untouched by this. */
|
|
40
|
+
type PiHarness = AgentHarness<ExecutionToolContext>;
|
|
41
|
+
export type PiHarnessFactory = (session: string) => PiHarness | Promise<PiHarness>;
|
|
31
42
|
export interface PiHarnessFactoryOptions {
|
|
32
43
|
/** Session persistence. Continuity = same backing store + same session id. */
|
|
33
44
|
sessions: PiSessionStore;
|
|
45
|
+
/** Filesystem/process environment for the default coding tools. Handed to the harness as the TURN's
|
|
46
|
+
* tool context (pi 0.83), which is how read/bash/edit/write reach the machine at all — so this is the
|
|
47
|
+
* ONE seam a sandbox adapter implements, not a knob beside the tools that ignore it. */
|
|
34
48
|
env: ExecutionEnv;
|
|
35
49
|
/** Provider collection for all model requests; {@link model} must belong to it (same provider id). */
|
|
36
50
|
models: Models;
|
|
@@ -38,7 +52,7 @@ export interface PiHarnessFactoryOptions {
|
|
|
38
52
|
/** Reasoning effort for the model (pi's scale). Unset = fastagent's pinned default ("medium", pi
|
|
39
53
|
* TUI parity — see {@link DEFAULT_THINKING_LEVEL}); unsupported levels are clamped by pi per model. */
|
|
40
54
|
thinkingLevel?: ThinkingLevel;
|
|
41
|
-
tools?:
|
|
55
|
+
tools?: MountedTool[];
|
|
42
56
|
/**
|
|
43
57
|
* Final assembled prompt, or a SYNC factory re-evaluated per invoke (how L1 serves dynamic
|
|
44
58
|
* `instructions` + the skills listing). Distinct from {@link live}, which is the directory rung's
|
|
@@ -71,36 +85,19 @@ export declare const SUMMARIZATION_RETRY_POLICY: {
|
|
|
71
85
|
readonly maxRetries: 3;
|
|
72
86
|
readonly baseDelayMs: 2000;
|
|
73
87
|
};
|
|
74
|
-
export declare const THINKING_LEVELS: ReadonlySet<ThinkingLevel>;
|
|
75
|
-
/** The shape both override consumers walk — a session entry, structurally. */
|
|
76
|
-
export interface OverrideEntryLike {
|
|
77
|
-
type: string;
|
|
78
|
-
provider?: string;
|
|
79
|
-
modelId?: string;
|
|
80
|
-
thinkingLevel?: string;
|
|
81
|
-
}
|
|
82
88
|
/**
|
|
83
|
-
* The
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
89
|
+
* The serving default for reasoning effort, pinned to what pi's TUI defaults to (its
|
|
90
|
+
* DEFAULT_THINKING_LEVEL) — NOT inherited from the bare harness, whose own fallback is "off": an
|
|
91
|
+
* author vibes at "medium" in pi and must get "medium" when served (fidelity), and pinning the value
|
|
92
|
+
* here means an upstream default change in either place cannot silently alter deployments. Models
|
|
93
|
+
* that don't support a level are clamped by pi per model.
|
|
88
94
|
*/
|
|
89
|
-
export declare
|
|
90
|
-
model?: {
|
|
91
|
-
provider: string;
|
|
92
|
-
modelId: string;
|
|
93
|
-
};
|
|
94
|
-
thinkingLevel?: string;
|
|
95
|
-
};
|
|
95
|
+
export declare const DEFAULT_THINKING_LEVEL: ThinkingLevel;
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* this adds the EXECUTION fallbacks: a recorded model no longer in this deployment's registry falls
|
|
102
|
-
* back to the default with a deduped warn (fail visibly without bricking the session — the
|
|
103
|
-
* conversation must survive a registry change across deploys); an unknown thinking level likewise.
|
|
97
|
+
* {@link resolveSessionSettings} plus the warn only the execution path owes: a recorded pair can stop
|
|
98
|
+
* being executable with no control-plane command involved (pi appends these entries itself; a
|
|
99
|
+
* deployment's configured model can change between restarts). Deduped per session+cause — it would
|
|
100
|
+
* otherwise repeat every turn.
|
|
104
101
|
*/
|
|
105
102
|
export declare function resolveHarnessOverrides(entries: OverrideEntryLike[], models: Models, defaults: {
|
|
106
103
|
model: AnyModel;
|
|
@@ -109,6 +106,7 @@ export declare function resolveHarnessOverrides(entries: OverrideEntryLike[], mo
|
|
|
109
106
|
model: AnyModel;
|
|
110
107
|
thinkingLevel: ThinkingLevel;
|
|
111
108
|
};
|
|
112
|
-
export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools:
|
|
109
|
+
export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools: MountedTool[], sessionId: string): string[] | undefined;
|
|
113
110
|
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing → create. */
|
|
114
111
|
export declare function piHarnessFactory(options: PiHarnessFactoryOptions): PiHarnessFactory;
|
|
112
|
+
export {};
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
10
10
|
import { log } from "../../log.js";
|
|
11
|
+
import { activePathEntries } from "./sessions.js";
|
|
11
12
|
import { isDeferredTool } from "./tool.js";
|
|
13
|
+
import { resolveSessionSettings } from "./session-settings.js";
|
|
12
14
|
/**
|
|
13
15
|
* The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
|
|
14
16
|
* tools a loader activated in that call. The DEDICATED record the resolve below reads: pi's own
|
|
@@ -18,10 +20,6 @@ import { isDeferredTool } from "./tool.js";
|
|
|
18
20
|
* carry only what was actually discovered.
|
|
19
21
|
*/
|
|
20
22
|
export const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
|
|
21
|
-
/** The session a factory-built harness is bound to — the seam the activation bridge (invoke.ts) uses
|
|
22
|
-
* to write {@link TOOL_ACTIVATION_ENTRY} deltas (pi's harness keeps its session private). Absent for
|
|
23
|
-
* a harness built outside {@link piHarnessFactory}: activation still works in-turn there, but is not
|
|
24
|
-
* recorded — the factory owns persistence. */
|
|
25
23
|
const harnessSessions = new WeakMap();
|
|
26
24
|
export function harnessSession(harness) {
|
|
27
25
|
return harnessSessions.get(harness);
|
|
@@ -52,7 +50,7 @@ export const SUMMARIZATION_RETRY_POLICY = { enabled: true, maxRetries: 3, baseDe
|
|
|
52
50
|
* here means an upstream default change in either place cannot silently alter deployments. Models
|
|
53
51
|
* that don't support a level are clamped by pi per model.
|
|
54
52
|
*/
|
|
55
|
-
const DEFAULT_THINKING_LEVEL = "medium";
|
|
53
|
+
export const DEFAULT_THINKING_LEVEL = "medium";
|
|
56
54
|
/**
|
|
57
55
|
* Resolve the active-tool set for a fresh harness — the ONE place both fallbacks live. pi's harness
|
|
58
56
|
* WRITES active-tool changes to the session (`setActiveTools` → `active_tools_change`) but its
|
|
@@ -76,83 +74,30 @@ const DEFAULT_THINKING_LEVEL = "medium";
|
|
|
76
74
|
* (like L2's findings memo), not session state — the resolve stays derived from the session.
|
|
77
75
|
*/
|
|
78
76
|
const warnedRestores = new Set();
|
|
79
|
-
/** pi's ThinkingLevel scale as a checkable set — THE single source for fastagent (session entries
|
|
80
|
-
* store plain strings; session-control's dispatch validation and capabilities derive from this).
|
|
81
|
-
* The `satisfies Record<ThinkingLevel, …>` anchor makes it EXHAUSTIVE against pi's union: pi
|
|
82
|
-
* adding a level turns this into a type error instead of a silent drift where `set_thinking`
|
|
83
|
-
* rejects a value pi supports. */
|
|
84
|
-
const ALL_THINKING_LEVELS = {
|
|
85
|
-
off: true,
|
|
86
|
-
minimal: true,
|
|
87
|
-
low: true,
|
|
88
|
-
medium: true,
|
|
89
|
-
high: true,
|
|
90
|
-
xhigh: true,
|
|
91
|
-
max: true,
|
|
92
|
-
};
|
|
93
|
-
export const THINKING_LEVELS = new Set(Object.keys(ALL_THINKING_LEVELS));
|
|
94
77
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* which record is "the" override.
|
|
100
|
-
*/
|
|
101
|
-
export function lastOverrideEntries(entries) {
|
|
102
|
-
let model;
|
|
103
|
-
let modelSeen = false;
|
|
104
|
-
let thinkingLevel;
|
|
105
|
-
let thinkingSeen = false;
|
|
106
|
-
for (let i = entries.length - 1; i >= 0 && !(modelSeen && thinkingSeen); i--) {
|
|
107
|
-
const e = entries[i];
|
|
108
|
-
if (!modelSeen && e?.type === "model_change") {
|
|
109
|
-
modelSeen = true;
|
|
110
|
-
if (e.provider !== undefined && e.modelId !== undefined)
|
|
111
|
-
model = { provider: e.provider, modelId: e.modelId };
|
|
112
|
-
}
|
|
113
|
-
if (!thinkingSeen && e?.type === "thinking_level_change") {
|
|
114
|
-
thinkingSeen = true;
|
|
115
|
-
if (e.thinkingLevel !== undefined)
|
|
116
|
-
thinkingLevel = e.thinkingLevel;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return { model, thinkingLevel };
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Resolve the session's model/thinking OVERRIDES for a fresh harness — same shape as the
|
|
123
|
-
* active-tools resolve above: pi writes `model_change`/`thinking_level_change` entries on explicit
|
|
124
|
-
* setModel/setThinkingLevel (the control plane's `set_model`/`set_thinking` append them directly)
|
|
125
|
-
* but a fresh harness never reads them back. Override facts come from {@link lastOverrideEntries};
|
|
126
|
-
* this adds the EXECUTION fallbacks: a recorded model no longer in this deployment's registry falls
|
|
127
|
-
* back to the default with a deduped warn (fail visibly without bricking the session — the
|
|
128
|
-
* conversation must survive a registry change across deploys); an unknown thinking level likewise.
|
|
78
|
+
* {@link resolveSessionSettings} plus the warn only the execution path owes: a recorded pair can stop
|
|
79
|
+
* being executable with no control-plane command involved (pi appends these entries itself; a
|
|
80
|
+
* deployment's configured model can change between restarts). Deduped per session+cause — it would
|
|
81
|
+
* otherwise repeat every turn.
|
|
129
82
|
*/
|
|
130
83
|
export function resolveHarnessOverrides(entries, models, defaults, sessionId) {
|
|
131
|
-
|
|
132
|
-
let thinkingLevel = defaults.thinkingLevel;
|
|
84
|
+
const settings = resolveSessionSettings(entries, models, defaults);
|
|
133
85
|
const warnOnce = (key, message) => {
|
|
134
86
|
const emit = warnedRestores.has(key) ? log.debug : log.warn;
|
|
135
87
|
warnedRestores.add(key);
|
|
136
88
|
emit(message);
|
|
137
89
|
};
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
if (found)
|
|
142
|
-
model = found;
|
|
143
|
-
else {
|
|
144
|
-
warnOnce(`${sessionId}\u0000model\u0000${recorded.model.provider}/${recorded.model.modelId}`, `[fastagent] session ${sessionId}: recorded model override ${recorded.model.provider}/${recorded.model.modelId} is not in this deployment's registry — using the configured default`);
|
|
145
|
-
}
|
|
90
|
+
const dropped = settings.dropped;
|
|
91
|
+
if (dropped?.model) {
|
|
92
|
+
warnOnce(`${sessionId}\u0000model\u0000${dropped.model}`, `[fastagent] session ${sessionId}: recorded model override ${dropped.model} is not in this deployment's registry — using the configured default`);
|
|
146
93
|
}
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
warnOnce(`${sessionId}\u0000thinking\u0000${recorded.thinkingLevel}`, `[fastagent] session ${sessionId}: recorded thinking level "${recorded.thinkingLevel}" is unknown — using the configured default`);
|
|
153
|
-
}
|
|
94
|
+
if (dropped?.thinkingLevel) {
|
|
95
|
+
const { recorded, running, known } = dropped.thinkingLevel;
|
|
96
|
+
warnOnce(`${sessionId}\u0000thinking\u0000${settings.model.provider}/${settings.model.id}\u0000${recorded}`, known
|
|
97
|
+
? `[fastagent] session ${sessionId}: recorded thinking level "${recorded}" is not supported by ${settings.model.provider}/${settings.model.id} — running at "${running}"`
|
|
98
|
+
: `[fastagent] session ${sessionId}: recorded thinking level "${recorded}" is unknown — using the configured default`);
|
|
154
99
|
}
|
|
155
|
-
return { model, thinkingLevel };
|
|
100
|
+
return { model: settings.model, thinkingLevel: settings.thinkingLevel };
|
|
156
101
|
}
|
|
157
102
|
export function resolveHarnessActiveToolNames(recorded, tools, sessionId) {
|
|
158
103
|
const anyDeferred = tools.some(isDeferredTool);
|
|
@@ -174,9 +119,10 @@ export function piHarnessFactory(options) {
|
|
|
174
119
|
return async (sessionId) => {
|
|
175
120
|
const session = await options.sessions.openOrCreate(sessionId);
|
|
176
121
|
// One extra entry walk per invoke to collect the activation deltas — negligible against the model
|
|
177
|
-
// call, same trade as L2's per-invoke definition re-read.
|
|
178
|
-
//
|
|
179
|
-
|
|
122
|
+
// call, same trade as L2's per-invoke definition re-read. The walk is over the ACTIVE PATH, not
|
|
123
|
+
// the flat journal: `navigate` moves the leaf, so the tree can hold an abandoned branch whose
|
|
124
|
+
// activations and overrides this session has left behind.
|
|
125
|
+
const entries = await activePathEntries(session);
|
|
180
126
|
const activated = entries.flatMap((e) => e.type === "custom" && e.customType === TOOL_ACTIVATION_ENTRY
|
|
181
127
|
? (e.data?.names ?? [])
|
|
182
128
|
: []);
|
|
@@ -187,7 +133,9 @@ export function piHarnessFactory(options) {
|
|
|
187
133
|
// Session overrides (set_model / set_thinking) win over the assembly defaults — same entry walk.
|
|
188
134
|
const overrides = resolveHarnessOverrides(entries, options.models, { model: options.model, thinkingLevel: options.thinkingLevel ?? DEFAULT_THINKING_LEVEL }, sessionId);
|
|
189
135
|
const harness = new AgentHarness({
|
|
190
|
-
|
|
136
|
+
// Static, not a per-turn provider: the env is fixed for the agent's lifetime, and resolving a
|
|
137
|
+
// constant per turn would only add a promise to the turn's critical path.
|
|
138
|
+
toolContext: { env: options.env },
|
|
191
139
|
session,
|
|
192
140
|
models: options.models,
|
|
193
141
|
model: overrides.model,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
1
|
import type { Agent } from "../../agent.ts";
|
|
3
2
|
import { type FastagentConfig } from "./config.ts";
|
|
4
3
|
import type { SessionControl } from "../../session.ts";
|
|
@@ -7,6 +6,7 @@ import type { PiSessionReader, PiSessionStore } from "./sessions.ts";
|
|
|
7
6
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
8
7
|
import type { LoadedDefinition } from "./definition.ts";
|
|
9
8
|
import type { ToolCollision } from "./tool.ts";
|
|
9
|
+
import type { MountedTool } from "./tool.ts";
|
|
10
10
|
export interface CreatePiAgentFromDirOptions {
|
|
11
11
|
/** Model spec override (e.g. the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
|
|
12
12
|
model?: string;
|
|
@@ -66,7 +66,7 @@ export interface AgentAssembly {
|
|
|
66
66
|
/** Absolute credentials file (--auth-path/authPath option > FASTAGENT_AUTH_PATH > <agentDir>/.secrets/auth.json). */
|
|
67
67
|
authPath: string;
|
|
68
68
|
/** The full mounted tool surface (config.tools + discovered tools/, search_tools applied). */
|
|
69
|
-
tools:
|
|
69
|
+
tools: MountedTool[];
|
|
70
70
|
toolNames: string[];
|
|
71
71
|
deferredToolNames: string[];
|
|
72
72
|
toolCollisions: ToolCollision[];
|
package/dist/engines/pi/open.js
CHANGED
|
@@ -25,7 +25,7 @@ export async function resolveAgentAssembly(dir, options = {}) {
|
|
|
25
25
|
if (!modelSpec) {
|
|
26
26
|
throw new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
|
|
27
27
|
}
|
|
28
|
-
const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir
|
|
28
|
+
const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir);
|
|
29
29
|
// The state root: sessions/channel state/schedule state derive from it (FASTAGENT_STATE_DIR moves it
|
|
30
30
|
// in one knob — a container points it at its volume); the finer overrides below still win.
|
|
31
31
|
const stateRoot = resolveStateRoot(agentDir);
|
|
@@ -102,6 +102,7 @@ export async function createPiAgentFromDir(dir, options = {}) {
|
|
|
102
102
|
lease: parts.lease,
|
|
103
103
|
models: parts.models,
|
|
104
104
|
harnessFactory: parts.harnessFactory,
|
|
105
|
+
defaults: parts.defaults,
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
108
|
: undefined,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReadImageProcessor } from "@earendil-works/pi-agent-core";
|
|
2
|
+
/** The `read` tool's image processor. Matches pi-coding-agent's messages verbatim: they reach the model
|
|
3
|
+
* as tool output, so a reworded one is a different prompt, not a different implementation detail. */
|
|
4
|
+
export declare const readImageProcessor: ReadImageProcessor;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The image pipeline pi's `read` tool needs: normalize an unsupported format to PNG, resize below the
|
|
3
|
+
* inline limit, and hand back the hints that tell the model what it is looking at.
|
|
4
|
+
*
|
|
5
|
+
* pi-agent-core's `createReadTool` takes this as an INJECTED processor and does nothing without one —
|
|
6
|
+
* unlike pi-coding-agent's, which wires its private `processImage` internally. That function is not
|
|
7
|
+
* exported (nor reachable: the package's `exports` map has no deep paths), so this rebuilds it from the
|
|
8
|
+
* two halves that ARE public, `convertToPng` and `resizeImage`/`formatDimensionNote`.
|
|
9
|
+
*
|
|
10
|
+
* It is upstream logic restated, which is a real cost — without it `read` on a screenshot sends the raw
|
|
11
|
+
* bytes (measured: 7.48 MB of base64 where pi-coding-agent sends 3.48 MB, and no dimension note for the
|
|
12
|
+
* model's coordinate math), and a bmp is dropped entirely while the tool's own description still
|
|
13
|
+
* advertises it. test/tools-parity.test.ts compares this against pi-coding-agent's real `read` on both
|
|
14
|
+
* paths, so upstream changing the pipeline surfaces as a failing test rather than as drift.
|
|
15
|
+
*/
|
|
16
|
+
import { convertToPng, formatDimensionNote, resizeImage } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
/** Formats a provider takes inline as-is; everything else has to become a PNG first. */
|
|
18
|
+
const INLINE_MIME = {
|
|
19
|
+
"image/png": "image/png",
|
|
20
|
+
"image/jpeg": "image/jpeg",
|
|
21
|
+
"image/jpg": "image/jpeg",
|
|
22
|
+
"image/gif": "image/gif",
|
|
23
|
+
"image/webp": "image/webp",
|
|
24
|
+
};
|
|
25
|
+
/** The `read` tool's image processor. Matches pi-coding-agent's messages verbatim: they reach the model
|
|
26
|
+
* as tool output, so a reworded one is a different prompt, not a different implementation detail. */
|
|
27
|
+
export const readImageProcessor = async (bytes, mimeType, options) => {
|
|
28
|
+
const base = mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
|
|
29
|
+
const inline = INLINE_MIME[base];
|
|
30
|
+
let normalized;
|
|
31
|
+
if (inline) {
|
|
32
|
+
normalized = { bytes, mimeType: inline };
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const png = await convertToPng(Buffer.from(bytes).toString("base64"), base);
|
|
36
|
+
if (!png)
|
|
37
|
+
return { ok: false, message: "[Image omitted: could not be converted to a supported inline image format.]" };
|
|
38
|
+
normalized = { bytes: Buffer.from(png.data, "base64"), mimeType: png.mimeType, convertedFrom: base };
|
|
39
|
+
}
|
|
40
|
+
const hints = [];
|
|
41
|
+
const converted = (to) => normalized.convertedFrom && normalized.convertedFrom !== to
|
|
42
|
+
? `[Image converted from ${normalized.convertedFrom} to ${to}.]`
|
|
43
|
+
: undefined;
|
|
44
|
+
if (!options.autoResizeImages) {
|
|
45
|
+
const hint = converted(normalized.mimeType);
|
|
46
|
+
if (hint)
|
|
47
|
+
hints.push(hint);
|
|
48
|
+
return { ok: true, data: Buffer.from(normalized.bytes).toString("base64"), mimeType: normalized.mimeType, hints };
|
|
49
|
+
}
|
|
50
|
+
const resized = await resizeImage(normalized.bytes, normalized.mimeType);
|
|
51
|
+
if (!resized)
|
|
52
|
+
return { ok: false, message: "[Image omitted: could not be resized below the inline image size limit.]" };
|
|
53
|
+
const hint = converted(resized.mimeType);
|
|
54
|
+
if (hint)
|
|
55
|
+
hints.push(hint);
|
|
56
|
+
// The scale factor the model needs to map coordinates back onto the original — dropping it is what
|
|
57
|
+
// makes a resized screenshot unusable for anything positional.
|
|
58
|
+
const note = formatDimensionNote(resized);
|
|
59
|
+
if (note)
|
|
60
|
+
hints.push(note);
|
|
61
|
+
return { ok: true, data: resized.data, mimeType: resized.mimeType, hints };
|
|
62
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type
|
|
2
|
-
/** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own.
|
|
3
|
-
|
|
1
|
+
import { type MountedTool } from "./tool.ts";
|
|
2
|
+
/** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own.
|
|
3
|
+
* Typed on the MOUNTED tool, not the authored one: it inspects names and deferral and never executes,
|
|
4
|
+
* so narrowing here would reject the very set it is handed (pi's defaults take the turn's context). */
|
|
5
|
+
export declare function withSearchTool(tools: MountedTool[]): MountedTool[];
|
|
4
6
|
/** Build the `search_tools` loader. Keyword search over the inactive tools' name+description.
|
|
5
7
|
*
|
|
6
8
|
* `executionMode: "sequential"` — pi turns any batch containing a sequential tool serial. Required for
|
|
7
9
|
* correct load-point attribution everywhere an OUTER active-set diff exists: pi wraps SDK customTools
|
|
8
10
|
* (the chat path) in a before/after diff, and two parallel loader calls would both snapshot the
|
|
9
11
|
* pre-activation set and get stamped with the same activation. Custom loader authors must set it too. */
|
|
10
|
-
export declare function makeSearchToolsTool():
|
|
12
|
+
export declare function makeSearchToolsTool(): MountedTool;
|