@fastagent-sh/fastagent 0.13.0 → 0.15.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/README.md +5 -5
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/context-buffer.d.ts +30 -0
- package/dist/channels/context-buffer.js +98 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +43 -0
- package/dist/channels/feishu/context-buffer.js +72 -0
- package/dist/channels/feishu/crypto.d.ts +4 -2
- package/dist/channels/feishu/crypto.js +4 -2
- package/dist/channels/feishu/feishu-api.d.ts +15 -7
- package/dist/channels/feishu/feishu-api.js +22 -4
- package/dist/channels/feishu/feishu.d.ts +38 -16
- package/dist/channels/feishu/feishu.js +286 -151
- package/dist/channels/feishu/invoke-turn.d.ts +24 -31
- package/dist/channels/feishu/invoke-turn.js +61 -62
- package/dist/channels/feishu/model.d.ts +98 -0
- package/dist/channels/feishu/model.js +9 -0
- package/dist/channels/feishu/normalize.d.ts +23 -0
- package/dist/channels/feishu/normalize.js +132 -0
- package/dist/channels/feishu/owned-threads.d.ts +7 -0
- package/dist/channels/feishu/owned-threads.js +47 -0
- package/dist/channels/feishu/parse.d.ts +21 -103
- package/dist/channels/feishu/parse.js +35 -145
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +26 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +10 -3
- package/dist/channels/feishu/setup-mode.d.ts +8 -0
- package/dist/channels/feishu/setup-mode.js +2 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +8 -6
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +35 -2
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/lark/scaffold/channel.ts +10 -3
- package/dist/channels/preview-kit.d.ts +109 -0
- package/dist/channels/preview-kit.js +183 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/owned-threads.d.ts +6 -0
- package/dist/channels/slack/owned-threads.js +43 -0
- package/dist/channels/slack/parse.d.ts +23 -0
- package/dist/channels/slack/parse.js +81 -0
- package/dist/channels/slack/preview.d.ts +24 -0
- package/dist/channels/slack/preview.js +359 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +77 -0
- package/dist/channels/slack/slack-api.js +415 -0
- package/dist/channels/slack/slack.d.ts +58 -0
- package/dist/channels/slack/slack.js +451 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.js +3 -3
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +13 -0
- package/dist/channels/tasks.js +10 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/telegram.js +23 -9
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
- package/dist/channels/{feishu/text.js → text.js} +1 -1
- package/dist/channels/turn-queue.js +1 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +27 -0
- package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
- package/dist/cli/add-slack.d.ts +10 -0
- package/dist/cli/add-slack.js +204 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +13 -0
- package/dist/cli/commands/add.js +274 -0
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +559 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.js +21 -0
- package/dist/cli/commands/deploy.d.ts +15 -0
- package/dist/cli/commands/deploy.js +394 -0
- package/dist/cli/commands/dev.d.ts +11 -0
- package/dist/cli/commands/dev.js +82 -0
- package/dist/cli/commands/fire.d.ts +7 -0
- package/dist/cli/commands/fire.js +45 -0
- package/dist/cli/commands/info.d.ts +7 -0
- package/dist/cli/commands/info.js +108 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.js +81 -0
- package/dist/cli/commands/invoke.d.ts +7 -0
- package/dist/cli/commands/invoke.js +28 -0
- package/dist/cli/commands/login.d.ts +6 -0
- package/dist/cli/commands/login.js +52 -0
- package/dist/cli/commands/models.d.ts +1 -0
- package/dist/cli/commands/models.js +15 -0
- package/dist/cli/commands/schedule.d.ts +12 -0
- package/dist/cli/commands/schedule.js +89 -0
- package/dist/cli/commands/start.d.ts +10 -0
- package/dist/cli/commands/start.js +97 -0
- package/dist/cli/commands/tool.d.ts +1 -0
- package/dist/cli/commands/tool.js +38 -0
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +32 -0
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +90 -0
- package/dist/cli/kernel.js +190 -0
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.d.ts +11 -0
- package/dist/cli/program.js +479 -0
- package/dist/cli/serve.d.ts +48 -0
- package/dist/cli/serve.js +248 -0
- package/dist/cli/shared.d.ts +44 -0
- package/dist/cli/shared.js +246 -0
- package/dist/cli.js +8 -1329
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/docker/plan.d.ts +45 -0
- package/dist/deploy/docker/plan.js +141 -0
- package/dist/deploy/docker/run.d.ts +40 -0
- package/dist/deploy/docker/run.js +126 -0
- package/dist/deploy/fly/plan.d.ts +3 -1
- package/dist/deploy/fly/plan.js +12 -7
- package/dist/deploy/fly/run.d.ts +5 -4
- package/dist/deploy/fly/run.js +16 -5
- package/dist/deploy/preflight.d.ts +10 -4
- package/dist/deploy/preflight.js +57 -12
- package/dist/deploy/railway/plan.d.ts +3 -1
- package/dist/deploy/railway/plan.js +12 -4
- package/dist/deploy/railway/run.d.ts +5 -4
- package/dist/deploy/railway/run.js +15 -5
- package/dist/deploy/runner.d.ts +6 -2
- package/dist/deploy/runner.js +1 -0
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +0 -2
- package/dist/dev-supervisor.js +7 -3
- package/dist/engines/pi/auth.js +160 -46
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +90 -60
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -188
- package/dist/engines/pi/config.d.ts +23 -9
- package/dist/engines/pi/config.js +35 -5
- package/dist/engines/pi/create.d.ts +36 -7
- package/dist/engines/pi/create.js +63 -22
- package/dist/engines/pi/harness.d.ts +65 -1
- package/dist/engines/pi/harness.js +166 -2
- package/dist/engines/pi/invoke.d.ts +56 -3
- package/dist/engines/pi/invoke.js +340 -20
- package/dist/engines/pi/login.d.ts +11 -0
- package/dist/engines/pi/login.js +17 -5
- package/dist/engines/pi/models.d.ts +56 -10
- package/dist/engines/pi/models.js +61 -23
- package/dist/engines/pi/search-tools.d.ts +10 -0
- package/dist/engines/pi/search-tools.js +138 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +46 -11
- package/dist/engines/pi/tool-context.js +11 -9
- package/dist/engines/pi/tool.d.ts +35 -6
- package/dist/engines/pi/tool.js +47 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/engines/pi/workspace.d.ts +56 -1
- package/dist/engines/pi/workspace.js +75 -16
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +23 -6
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/pi.d.ts +6 -4
- package/dist/pi.js +2 -1
- package/dist/scaffold/add-channel.d.ts +19 -6
- package/dist/scaffold/add-channel.js +127 -16
- package/dist/scaffold/templates/fastagent.config.mjs +5 -3
- package/dist/schedule/wakeups.d.ts +0 -3
- package/dist/schedule/wakeups.js +1 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +21 -10
- package/package.json +22 -7
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
|
@@ -14,10 +14,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
14
14
|
* live in persona.md + skills, with AGENTS.md as project context). Deleting the config still leaves a
|
|
15
15
|
* zero-config agent runnable with a model supplied by --model / FASTAGENT_MODEL.
|
|
16
16
|
*/
|
|
17
|
-
import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
17
|
+
import { existsSync, lstatSync, realpathSync, statSync } from "node:fs";
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
19
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
20
20
|
import { pathToFileURL } from "node:url";
|
|
21
|
+
import { THINKING_LEVELS } from "./harness.js";
|
|
21
22
|
import { moduleLoadHint } from "../../loader.js";
|
|
22
23
|
/** Identity function for typing and IDE completion (vite/next-style). */
|
|
23
24
|
export function defineConfig(config) {
|
|
@@ -56,7 +57,14 @@ export async function loadConfig(dir) {
|
|
|
56
57
|
const path = found[0];
|
|
57
58
|
let mod;
|
|
58
59
|
try {
|
|
59
|
-
|
|
60
|
+
// Cache-bust on file change: ESM `import()` caches by URL, so a config REWRITTEN in this process
|
|
61
|
+
// (the first-run picker's write-back) would otherwise read back stale — deploy's model-travel gate
|
|
62
|
+
// then contradicts the "saved model" line it just printed. mtime keeps unchanged files cached;
|
|
63
|
+
// its resolution is the ceiling — a rewrite within the same timestamp tick reads stale (fine for
|
|
64
|
+
// the write-back: sub-tick only on coarse-mtime filesystems, and the next process starts fresh).
|
|
65
|
+
const url = pathToFileURL(path);
|
|
66
|
+
url.searchParams.set("v", String(statSync(path).mtimeMs));
|
|
67
|
+
mod = (await import(__rewriteRelativeImportExtension(url.href)));
|
|
60
68
|
}
|
|
61
69
|
catch (error) {
|
|
62
70
|
throw new Error(`${path}: ${error.message}${moduleLoadHint(error)}`);
|
|
@@ -70,17 +78,25 @@ export async function loadConfig(dir) {
|
|
|
70
78
|
// (`modle:`) must not silently degrade to zero-config.
|
|
71
79
|
for (const key of Object.keys(c)) {
|
|
72
80
|
if (key !== "model" &&
|
|
81
|
+
key !== "thinkingLevel" &&
|
|
73
82
|
key !== "agentDir" &&
|
|
74
83
|
key !== "tools" &&
|
|
75
84
|
key !== "http" &&
|
|
76
85
|
key !== "deploy" &&
|
|
77
|
-
key !== "selfSchedule"
|
|
78
|
-
|
|
86
|
+
key !== "selfSchedule" &&
|
|
87
|
+
key !== "sessionControl") {
|
|
88
|
+
throw new Error(`${path}: unknown key "${key}" (valid keys: model, thinkingLevel, agentDir, tools, http, deploy, selfSchedule, sessionControl)`);
|
|
79
89
|
}
|
|
80
90
|
}
|
|
81
91
|
if (c.model !== undefined && typeof c.model !== "string") {
|
|
82
92
|
throw new Error(`${path}: "model" must be a "provider/modelId" string`);
|
|
83
93
|
}
|
|
94
|
+
if (c.sessionControl !== undefined && typeof c.sessionControl !== "boolean") {
|
|
95
|
+
throw new Error(`${path}: "sessionControl" must be a boolean`);
|
|
96
|
+
}
|
|
97
|
+
if (c.thinkingLevel !== undefined && !THINKING_LEVELS.has(c.thinkingLevel)) {
|
|
98
|
+
throw new Error(`${path}: "thinkingLevel" must be one of ${[...THINKING_LEVELS].join(", ")}`);
|
|
99
|
+
}
|
|
84
100
|
if (c.agentDir !== undefined && typeof c.agentDir !== "string") {
|
|
85
101
|
throw new Error(`${path}: "agentDir" must be a string (a subdirectory relative to the config file)`);
|
|
86
102
|
}
|
|
@@ -174,6 +190,13 @@ export async function loadConfig(dir) {
|
|
|
174
190
|
export function resolveAgentDir(dir, config) {
|
|
175
191
|
return resolve(dir, config.agentDir ?? ".");
|
|
176
192
|
}
|
|
193
|
+
/** The provider prefix of a "provider/modelId" spec. A spec without "/" returns whole — downstream
|
|
194
|
+
* lookups then miss visibly (an unknown-provider error / a login-required hint), never a mangled id
|
|
195
|
+
* (`slice(0, indexOf("/"))` silently drops the last char when "/" is absent). */
|
|
196
|
+
export function providerOf(spec) {
|
|
197
|
+
const slash = spec.indexOf("/");
|
|
198
|
+
return slash > 0 ? spec.slice(0, slash) : spec;
|
|
199
|
+
}
|
|
177
200
|
/** Resolve "provider/modelId" → a pi Model from `models`, so the harness resolves auth from the same collection. */
|
|
178
201
|
export function resolveModel(models, spec) {
|
|
179
202
|
const slash = spec.indexOf("/");
|
|
@@ -212,6 +235,13 @@ export function rewriteConfigModel(src, spec) {
|
|
|
212
235
|
return src.replace(commented, line);
|
|
213
236
|
if (active.test(src))
|
|
214
237
|
return src.replace(active, line);
|
|
238
|
+
// No model line at all — the natural state after "picked once, then hand-deleted the line to reset".
|
|
239
|
+
// Re-INSERT at the top of the default-export object while the config still has the scaffold's block
|
|
240
|
+
// shape; anything else (a wrapper call, a one-liner, a computed export) is hand-shaped — leave it
|
|
241
|
+
// untouched (the caller prints the set-it-yourself hint).
|
|
242
|
+
const opener = /^export default[ \t]*\{[ \t]*$/m;
|
|
243
|
+
if (opener.test(src))
|
|
244
|
+
return src.replace(opener, (open) => `${open}\n${line}`);
|
|
215
245
|
return null;
|
|
216
246
|
}
|
|
217
247
|
/** Model selection precedence: CLI flag > FASTAGENT_MODEL env > config default. */
|
|
@@ -253,7 +283,7 @@ export function resolveAuthPathOverride(flag, env = process.env) {
|
|
|
253
283
|
* The IN-TREE default state root, `<dir>/.fastagent` — what {@link resolveStateRoot} falls back to when
|
|
254
284
|
* `FASTAGENT_STATE_DIR` moves state nowhere. THE single definition of that path segment.
|
|
255
285
|
*/
|
|
256
|
-
|
|
286
|
+
function projectStateDir(dir) {
|
|
257
287
|
return join(dir, ".fastagent");
|
|
258
288
|
}
|
|
259
289
|
/**
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import type { AgentTool, ExecutionEnv, Skill } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { Provider } from "@earendil-works/pi-ai";
|
|
1
|
+
import type { AgentTool, ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
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
7
|
import { type PiSessionStore } from "./sessions.ts";
|
|
7
8
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
8
|
-
import { type ToolCollision } from "./tool.ts";
|
|
9
|
-
import { type Lease } from "./invoke.ts";
|
|
9
|
+
import { type FastagentTool, type ToolCollision } from "./tool.ts";
|
|
10
|
+
import { type Lease, type SessionObserver } from "./invoke.ts";
|
|
10
11
|
/** pi's core default toolset (read/bash/edit/write), rooted at cwd. */
|
|
11
12
|
export declare function piDefaultTools(cwd: string): AgentTool[];
|
|
12
13
|
/** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
|
|
@@ -19,6 +20,9 @@ export declare function resolveTools(config: FastagentConfig, cwd: string): Agen
|
|
|
19
20
|
export declare function resolveWorkspaceTools(config: FastagentConfig, agentDir: string, cwd?: string): Promise<{
|
|
20
21
|
tools: AgentTool[];
|
|
21
22
|
toolNames: string[];
|
|
23
|
+
/** Tools registered but not initially active (defineTool `deferred: true`) — discovered/activated
|
|
24
|
+
* via the built-in `search_tools` loader. Surfaced so the operator can see deferral took effect. */
|
|
25
|
+
deferredToolNames: string[];
|
|
22
26
|
toolCollisions: ToolCollision[];
|
|
23
27
|
toolFailures: ModuleLoadFailure[];
|
|
24
28
|
}>;
|
|
@@ -49,17 +53,31 @@ export interface AssembleSystemPromptOptions {
|
|
|
49
53
|
cwd?: string;
|
|
50
54
|
}
|
|
51
55
|
export declare function assembleSystemPrompt(options: AssembleSystemPromptOptions): string;
|
|
56
|
+
/**
|
|
57
|
+
* INTERNAL seam (workspace ↔ assembly): hands the hub-wiring consumer the assembly's live parts —
|
|
58
|
+
* the SAME models collection, harness factory, and lease the agent runs with — so boundary
|
|
59
|
+
* mutations (session-control.ts) contend on the real lease and validate against the real registry.
|
|
60
|
+
* Called synchronously, exactly once, before the agent is returned. Not part of the public surface.
|
|
61
|
+
*/
|
|
62
|
+
type OnAssembly = (parts: {
|
|
63
|
+
models: Models;
|
|
64
|
+
harnessFactory: ReturnType<typeof piHarnessFactory>;
|
|
65
|
+
lease: Lease;
|
|
66
|
+
}) => void;
|
|
52
67
|
/** L1 options. Tier 1: model (spec) + instructions + tools. Tier 2: the injectable ports. */
|
|
53
68
|
export interface CreatePiAgentOptions {
|
|
54
69
|
/** Model spec "provider/modelId" (e.g. "openai-codex/gpt-5.5"), resolved against {@link models}. */
|
|
55
70
|
model: string;
|
|
71
|
+
/** Reasoning effort (pi's scale). Unset = pi's default; unsupported levels are clamped per model. */
|
|
72
|
+
thinkingLevel?: ThinkingLevel;
|
|
56
73
|
/**
|
|
57
74
|
* The system prompt itself — verbatim, no engine base and no wrapping (unlike the directory path,
|
|
58
75
|
* which assembles the engine base + AGENTS.md as segment ② + persona.md as segment ①). A plain string
|
|
59
76
|
* or a factory re-evaluated per invoke. When {@link skills} are mounted their listing is appended.
|
|
60
77
|
*/
|
|
61
78
|
instructions?: string | (() => string);
|
|
62
|
-
|
|
79
|
+
/** `FastagentTool` = AgentTool plus the optional `deferred` marker (see {@link DefineToolOptions}). */
|
|
80
|
+
tools?: FastagentTool[];
|
|
63
81
|
skills?: Skill[];
|
|
64
82
|
/**
|
|
65
83
|
* Extra providers registered on top of the built-ins — your own gateway / self-hosted endpoint /
|
|
@@ -80,6 +98,9 @@ export interface CreatePiAgentOptions {
|
|
|
80
98
|
env?: ExecutionEnv;
|
|
81
99
|
/** Single-writer lease. Defaults to in-process fail-fast inProcessLease(). */
|
|
82
100
|
lease?: Lease;
|
|
101
|
+
/** Observation-plane tap (session control): every rich session event of every run. Wire the one
|
|
102
|
+
* returned by `createPiSessionControl` to serve `state`/`entries`/`events` for this agent. */
|
|
103
|
+
observer?: SessionObserver;
|
|
83
104
|
}
|
|
84
105
|
/** L1: assemble from typed parts. */
|
|
85
106
|
export declare function createPiAgent(options: CreatePiAgentOptions): Agent;
|
|
@@ -90,11 +111,14 @@ export declare function createPiAgent(options: CreatePiAgentOptions): Agent;
|
|
|
90
111
|
export interface CreatePiAgentFromDefinitionOptions {
|
|
91
112
|
/** Model spec "provider/modelId", resolved against {@link models}. */
|
|
92
113
|
model: string;
|
|
114
|
+
/** Reasoning effort (pi's scale). Unset = pi's default; unsupported levels are clamped per model. */
|
|
115
|
+
thinkingLevel?: ThinkingLevel;
|
|
93
116
|
/** Override the engine base prompt (segment ①). Defaults to piBasePrompt({ tools, persona }) using the
|
|
94
117
|
* live-read persona.md; pass base to fully opt out of persona.md. */
|
|
95
118
|
base?: string;
|
|
96
|
-
/** Override tools. Defaults to piDefaultTools (lock down with a custom list).
|
|
97
|
-
|
|
119
|
+
/** Override tools. Defaults to piDefaultTools (lock down with a custom list). `FastagentTool` =
|
|
120
|
+
* AgentTool plus the optional `deferred` marker. */
|
|
121
|
+
tools?: FastagentTool[];
|
|
98
122
|
/**
|
|
99
123
|
* The agent's working directory: where the default tools operate AND whose ancestors are walked for
|
|
100
124
|
* ② project context (AGENTS.md). Defaults to `dir` (flat: the definition dir is also the run root).
|
|
@@ -115,6 +139,10 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
115
139
|
* context loader remain local today, so injecting this alone does not sandbox a directory agent. */
|
|
116
140
|
env?: ExecutionEnv;
|
|
117
141
|
lease?: Lease;
|
|
142
|
+
/** Observation-plane tap; see {@link CreatePiAgentOptions.observer}. */
|
|
143
|
+
observer?: SessionObserver;
|
|
144
|
+
/** INTERNAL seam for hub wiring; see {@link OnAssembly}. */
|
|
145
|
+
onAssembly?: OnAssembly;
|
|
118
146
|
}
|
|
119
147
|
/**
|
|
120
148
|
* L2: "point at a directory → agent": load + assemble (base + AGENTS.md + skills + env) + L1 in one
|
|
@@ -124,3 +152,4 @@ export declare function createPiAgentFromDefinition(dir: string, options: Create
|
|
|
124
152
|
agent: Agent;
|
|
125
153
|
definition: LoadedDefinition;
|
|
126
154
|
}>;
|
|
155
|
+
export {};
|
|
@@ -19,8 +19,9 @@ import { piHarnessFactory } from "./harness.js";
|
|
|
19
19
|
import { createPiModels } from "./models.js";
|
|
20
20
|
import { reportDefinitionWarnings } from "./report.js";
|
|
21
21
|
import { inMemorySessionStore } from "./sessions.js";
|
|
22
|
-
import { loadTools, mergeDiscoveredTools } from "./tool.js";
|
|
23
|
-
import {
|
|
22
|
+
import { isDeferredTool, loadTools, mergeDiscoveredTools, } from "./tool.js";
|
|
23
|
+
import { withSearchTool } from "./search-tools.js";
|
|
24
|
+
import { createPiAgentFromHarness, inProcessLease } from "./invoke.js";
|
|
24
25
|
// ── §1 tools ─────────────────────────────────────────────────────────────────
|
|
25
26
|
//
|
|
26
27
|
// The full pi toolset is the default for fidelity: authors vibe in local pi with it, so serving with
|
|
@@ -44,11 +45,30 @@ export async function resolveWorkspaceTools(config, agentDir, cwd = agentDir) {
|
|
|
44
45
|
// Default coding tools (read/bash/edit/write) are rooted at `cwd` (the run root the agent operates on);
|
|
45
46
|
// discovered `tools/` come from `agentDir` (the agent's own surface). They coincide in the flat case.
|
|
46
47
|
const discovered = await loadTools(agentDir);
|
|
47
|
-
const
|
|
48
|
-
|
|
48
|
+
const merged = mergeDiscoveredTools(resolveTools(config, cwd), discovered.tools);
|
|
49
|
+
// The built-in `search_tools` loader mounts here — the one place the workspace's full tool set is
|
|
50
|
+
// computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; a
|
|
51
|
+
// workspace-defined search_tools wins).
|
|
52
|
+
const tools = withSearchTool(merged.tools);
|
|
53
|
+
// Builtin = a search_tools that was ABSENT before withSearchTool (a reference compare would misfire
|
|
54
|
+
// on the deferred-authored-loader case, where withSearchTool returns a new array without adding one).
|
|
55
|
+
const builtinLoaderMounted = !merged.tools.some((t) => t.name === "search_tools") && tools.some((t) => t.name === "search_tools");
|
|
56
|
+
const toolCollisions = [...discovered.collisions, ...merged.collisions];
|
|
57
|
+
// `toolNames` is the AUTHOR's active-by-default surface (config.tools + tools/): exclude pi
|
|
58
|
+
// defaults, the builtin loader (like wake, a builtin gets its own report line, not an anonymous
|
|
59
|
+
// slot in the author's list — an author-DEFINED search_tools still shows), and deferred tools —
|
|
60
|
+
// each name lives in exactly ONE report slot, and deferred names live in `deferredToolNames`.
|
|
49
61
|
const defaultNames = new Set(piDefaultTools(cwd).map((t) => t.name));
|
|
50
|
-
const toolNames = tools
|
|
51
|
-
|
|
62
|
+
const toolNames = tools
|
|
63
|
+
.filter((t) => !defaultNames.has(t.name) && !isDeferredTool(t) && !(builtinLoaderMounted && t.name === "search_tools"))
|
|
64
|
+
.map((t) => t.name);
|
|
65
|
+
return {
|
|
66
|
+
tools,
|
|
67
|
+
toolNames,
|
|
68
|
+
deferredToolNames: tools.filter(isDeferredTool).map((t) => t.name),
|
|
69
|
+
toolCollisions,
|
|
70
|
+
toolFailures: discovered.failures,
|
|
71
|
+
};
|
|
52
72
|
}
|
|
53
73
|
// ── §2 prompt: four-segment systemPrompt assembly ───────────────────────────
|
|
54
74
|
//
|
|
@@ -69,16 +89,25 @@ export async function resolveWorkspaceTools(config, agentDir, cwd = agentDir) {
|
|
|
69
89
|
* `persona` (from persona.md) replaces the default identity line, keeping the tools list + guidelines.
|
|
70
90
|
*/
|
|
71
91
|
export function piBasePrompt(options = {}) {
|
|
72
|
-
const
|
|
92
|
+
const mounted = options.tools ?? [];
|
|
93
|
+
// Deferred tools stay OUT of the list: their schemas are not in the request until activated, so
|
|
94
|
+
// naming them here would invite calls to tools that don't exist yet; discovery is search_tools' job
|
|
95
|
+
// (which IS listed — it's active). Computed from the static mounted set, so the prompt — the cached
|
|
96
|
+
// context prefix — does not change when a tool is activated mid-session.
|
|
97
|
+
const tools = mounted.filter((t) => !isDeferredTool(t));
|
|
98
|
+
const deferredCount = mounted.length - tools.length;
|
|
73
99
|
const toolsList = tools.length > 0 ? tools.map((t) => `- ${t.name}: ${(t.description ?? "").split("\n")[0]}`).join("\n") : "(none)";
|
|
74
100
|
// Segment ① identity: an authored persona (persona.md) replaces the default engine identity line
|
|
75
101
|
// (the standalone×code-repo cell's persona; core.md §11), keeping the tools list + guidelines below.
|
|
76
102
|
const identity = options.persona?.trim() ||
|
|
77
103
|
"You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
104
|
+
const deferredNote = deferredCount > 0
|
|
105
|
+
? `\n\n${deferredCount} additional tool(s) are registered but inactive — use search_tools to discover and activate them before concluding a capability is missing.`
|
|
106
|
+
: "";
|
|
78
107
|
return `${identity}
|
|
79
108
|
|
|
80
109
|
Available tools:
|
|
81
|
-
${toolsList}
|
|
110
|
+
${toolsList}${deferredNote}
|
|
82
111
|
|
|
83
112
|
In addition to the tools above, you may have access to other custom tools depending on the project.
|
|
84
113
|
|
|
@@ -112,19 +141,23 @@ export function assembleSystemPrompt(options) {
|
|
|
112
141
|
*/
|
|
113
142
|
function buildPiAgent(opts) {
|
|
114
143
|
const models = createPiModels({ providers: opts.providers, authPath: opts.authPath });
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
const env = opts.env ?? new NodeExecutionEnv({ cwd: process.cwd() });
|
|
145
|
+
// Materialized here (not defaulted inside createPiAgentFromHarness) so the exposed parts carry
|
|
146
|
+
// the SAME lease instance the agent runs under — boundary mutations must contend on it.
|
|
147
|
+
const lease = opts.lease ?? inProcessLease();
|
|
148
|
+
const harnessFactory = piHarnessFactory({
|
|
149
|
+
sessions: opts.sessions ?? inMemorySessionStore(),
|
|
150
|
+
env,
|
|
151
|
+
models,
|
|
152
|
+
model: resolveModel(models, opts.model),
|
|
153
|
+
thinkingLevel: opts.thinkingLevel,
|
|
154
|
+
systemPrompt: opts.systemPrompt,
|
|
155
|
+
tools: opts.tools,
|
|
156
|
+
skills: opts.skills,
|
|
157
|
+
live: opts.live,
|
|
127
158
|
});
|
|
159
|
+
opts.onAssembly?.({ models, harnessFactory, lease });
|
|
160
|
+
return createPiAgentFromHarness({ lease, observer: opts.observer, cwd: env.cwd, harnessFactory });
|
|
128
161
|
}
|
|
129
162
|
/**
|
|
130
163
|
* L1 system prompt: `instructions` ARE the prompt (no engine base, no wrapping); the skills listing
|
|
@@ -145,14 +178,17 @@ function instructionsPrompt(instructions, skills) {
|
|
|
145
178
|
export function createPiAgent(options) {
|
|
146
179
|
return buildPiAgent({
|
|
147
180
|
model: options.model,
|
|
181
|
+
thinkingLevel: options.thinkingLevel,
|
|
148
182
|
providers: options.providers,
|
|
149
183
|
authPath: options.authPath,
|
|
150
184
|
systemPrompt: instructionsPrompt(options.instructions, options.skills),
|
|
151
|
-
tools
|
|
185
|
+
// Deferred tools need their loader on every rung (idempotent; the caller's own search_tools wins).
|
|
186
|
+
tools: options.tools ? withSearchTool(options.tools) : options.tools,
|
|
152
187
|
skills: options.skills,
|
|
153
188
|
sessions: options.sessions,
|
|
154
189
|
env: options.env,
|
|
155
190
|
lease: options.lease,
|
|
191
|
+
observer: options.observer,
|
|
156
192
|
});
|
|
157
193
|
}
|
|
158
194
|
/** Stable identity of a definition's non-fatal findings, for change-detection in `live` (dedup only). */
|
|
@@ -177,9 +213,12 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
177
213
|
// runtime-written bad skill surfaces the moment it appears, while a static finding does not spam
|
|
178
214
|
// every turn's log. A log-dedup memo, not session state (stateless invoke holds).
|
|
179
215
|
let reportedFindings = findingsSignature(definition);
|
|
180
|
-
|
|
216
|
+
// Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
|
|
217
|
+
// it; a caller's own search_tools wins).
|
|
218
|
+
const tools = withSearchTool(options.tools ?? piDefaultTools(env.cwd));
|
|
181
219
|
const agent = buildPiAgent({
|
|
182
220
|
model: options.model,
|
|
221
|
+
thinkingLevel: options.thinkingLevel,
|
|
183
222
|
providers: options.providers,
|
|
184
223
|
// Dir-aware default: the same state-root-derived file the opener uses for this dir (the opener
|
|
185
224
|
// passes an explicit authPath, so this only affects direct L2 callers).
|
|
@@ -218,6 +257,8 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
218
257
|
sessions: options.sessions,
|
|
219
258
|
env,
|
|
220
259
|
lease: options.lease,
|
|
260
|
+
observer: options.observer,
|
|
261
|
+
onAssembly: options.onAssembly,
|
|
221
262
|
});
|
|
222
263
|
return { agent, definition };
|
|
223
264
|
}
|
|
@@ -7,9 +7,20 @@
|
|
|
7
7
|
* historical entries back into context via buildContext().
|
|
8
8
|
*/
|
|
9
9
|
import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
10
|
-
import type { AgentTool, ExecutionEnv, Skill } from "@earendil-works/pi-agent-core";
|
|
10
|
+
import type { AgentTool, ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
11
11
|
import type { Model, Models } from "@earendil-works/pi-ai";
|
|
12
12
|
import type { PiSessionStore } from "./sessions.ts";
|
|
13
|
+
/**
|
|
14
|
+
* The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
|
|
15
|
+
* tools a loader activated in that call. The DEDICATED record the resolve below reads: pi's own
|
|
16
|
+
* `active_tools_change` entries are full active-set SNAPSHOTS (setActiveTools persists everything
|
|
17
|
+
* active at that moment), and reinterpreting a snapshot as activations would keep a tool active in
|
|
18
|
+
* old sessions after the author flips it to `deferred` — the session never discovered it. Deltas
|
|
19
|
+
* carry only what was actually discovered.
|
|
20
|
+
*/
|
|
21
|
+
export declare const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
|
|
22
|
+
export type PiSession = Awaited<ReturnType<PiSessionStore["openOrCreate"]>>;
|
|
23
|
+
export declare function harnessSession(harness: AgentHarness): PiSession | undefined;
|
|
13
24
|
/**
|
|
14
25
|
* pi's Model with the API-shape generic erased — fastagent only passes models through to the
|
|
15
26
|
* harness, so the generic carries no information. One alias keeps the `any` auditable.
|
|
@@ -24,6 +35,9 @@ export interface PiHarnessFactoryOptions {
|
|
|
24
35
|
/** Provider collection for all model requests; {@link model} must belong to it (same provider id). */
|
|
25
36
|
models: Models;
|
|
26
37
|
model: AnyModel;
|
|
38
|
+
/** Reasoning effort for the model (pi's scale). Unset = fastagent's pinned default ("medium", pi
|
|
39
|
+
* TUI parity — see {@link DEFAULT_THINKING_LEVEL}); unsupported levels are clamped by pi per model. */
|
|
40
|
+
thinkingLevel?: ThinkingLevel;
|
|
27
41
|
tools?: AgentTool[];
|
|
28
42
|
/**
|
|
29
43
|
* Final assembled prompt, or a SYNC factory re-evaluated per invoke (how L1 serves dynamic
|
|
@@ -46,5 +60,55 @@ export interface PiHarnessFactoryOptions {
|
|
|
46
60
|
skills?: Skill[];
|
|
47
61
|
}>;
|
|
48
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Retry policy for generated compaction/branch-summary model calls (pi ≥0.81.1, #6901). OPT-IN
|
|
65
|
+
* upstream — an undefined policy means no retries — so both compaction paths pass it explicitly:
|
|
66
|
+
* the harness config (auto-compaction inside a run) and the manual `compact()` dispatch in
|
|
67
|
+
* session-control. Values mirror pi's own app defaults (maxRetries 3, base 2s exponential).
|
|
68
|
+
*/
|
|
69
|
+
export declare const SUMMARIZATION_RETRY_POLICY: {
|
|
70
|
+
readonly enabled: true;
|
|
71
|
+
readonly maxRetries: 3;
|
|
72
|
+
readonly baseDelayMs: 2000;
|
|
73
|
+
};
|
|
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
|
+
/**
|
|
83
|
+
* The session's durable override FACTS — the ONE walk both surfaces consume (`state()` reports the
|
|
84
|
+
* recorded truth; `resolveHarnessOverrides` below applies registry/scale fallbacks on top). The
|
|
85
|
+
* LAST entry of each kind wins, and a malformed record reads as ABSENT for that kind — never
|
|
86
|
+
* skipped over to an earlier record: the reporting surface and the execution surface must agree on
|
|
87
|
+
* which record is "the" override.
|
|
88
|
+
*/
|
|
89
|
+
export declare function lastOverrideEntries(entries: OverrideEntryLike[]): {
|
|
90
|
+
model?: {
|
|
91
|
+
provider: string;
|
|
92
|
+
modelId: string;
|
|
93
|
+
};
|
|
94
|
+
thinkingLevel?: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the session's model/thinking OVERRIDES for a fresh harness — same shape as the
|
|
98
|
+
* active-tools resolve above: pi writes `model_change`/`thinking_level_change` entries on explicit
|
|
99
|
+
* setModel/setThinkingLevel (the control plane's `set_model`/`set_thinking` append them directly)
|
|
100
|
+
* but a fresh harness never reads them back. Override facts come from {@link lastOverrideEntries};
|
|
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.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveHarnessOverrides(entries: OverrideEntryLike[], models: Models, defaults: {
|
|
106
|
+
model: AnyModel;
|
|
107
|
+
thinkingLevel: ThinkingLevel;
|
|
108
|
+
}, sessionId: string): {
|
|
109
|
+
model: AnyModel;
|
|
110
|
+
thinkingLevel: ThinkingLevel;
|
|
111
|
+
};
|
|
112
|
+
export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools: AgentTool[], sessionId: string): string[] | undefined;
|
|
49
113
|
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing → create. */
|
|
50
114
|
export declare function piHarnessFactory(options: PiHarnessFactoryOptions): PiHarnessFactory;
|
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
* historical entries back into context via buildContext().
|
|
8
8
|
*/
|
|
9
9
|
import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
10
|
+
import { log } from "../../log.js";
|
|
11
|
+
import { isDeferredTool } from "./tool.js";
|
|
12
|
+
/**
|
|
13
|
+
* The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
|
|
14
|
+
* tools a loader activated in that call. The DEDICATED record the resolve below reads: pi's own
|
|
15
|
+
* `active_tools_change` entries are full active-set SNAPSHOTS (setActiveTools persists everything
|
|
16
|
+
* active at that moment), and reinterpreting a snapshot as activations would keep a tool active in
|
|
17
|
+
* old sessions after the author flips it to `deferred` — the session never discovered it. Deltas
|
|
18
|
+
* carry only what was actually discovered.
|
|
19
|
+
*/
|
|
20
|
+
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
|
+
const harnessSessions = new WeakMap();
|
|
26
|
+
export function harnessSession(harness) {
|
|
27
|
+
return harnessSessions.get(harness);
|
|
28
|
+
}
|
|
10
29
|
/**
|
|
11
30
|
* Provider request retries. The OpenAI-family / Anthropic / Azure / Codex pi-ai adapters
|
|
12
31
|
* implement client-side retries (429/5xx/request-phase network failures with backoff, honoring
|
|
@@ -19,23 +38,168 @@ import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
|
19
38
|
* so a mid-stream failure surfaces as a `failed` event.
|
|
20
39
|
*/
|
|
21
40
|
const PROVIDER_MAX_RETRIES = 2;
|
|
41
|
+
/**
|
|
42
|
+
* Retry policy for generated compaction/branch-summary model calls (pi ≥0.81.1, #6901). OPT-IN
|
|
43
|
+
* upstream — an undefined policy means no retries — so both compaction paths pass it explicitly:
|
|
44
|
+
* the harness config (auto-compaction inside a run) and the manual `compact()` dispatch in
|
|
45
|
+
* session-control. Values mirror pi's own app defaults (maxRetries 3, base 2s exponential).
|
|
46
|
+
*/
|
|
47
|
+
export const SUMMARIZATION_RETRY_POLICY = { enabled: true, maxRetries: 3, baseDelayMs: 2000 };
|
|
48
|
+
/**
|
|
49
|
+
* The serving default for reasoning effort, pinned to what pi's TUI defaults to (its
|
|
50
|
+
* DEFAULT_THINKING_LEVEL) — NOT inherited from the bare harness, whose own fallback is "off": an
|
|
51
|
+
* author vibes at "medium" in pi and must get "medium" when served (fidelity), and pinning the value
|
|
52
|
+
* here means an upstream default change in either place cannot silently alter deployments. Models
|
|
53
|
+
* that don't support a level are clamped by pi per model.
|
|
54
|
+
*/
|
|
55
|
+
const DEFAULT_THINKING_LEVEL = "medium";
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the active-tool set for a fresh harness — the ONE place both fallbacks live. pi's harness
|
|
58
|
+
* WRITES active-tool changes to the session (`setActiveTools` → `active_tools_change`) but its
|
|
59
|
+
* constructor never reads them back — pi's long-lived TUI harness keeps the set in memory, while
|
|
60
|
+
* fastagent builds a FRESH harness per invoke, which would silently reset the session's active set
|
|
61
|
+
* every turn.
|
|
62
|
+
*
|
|
63
|
+
* No record (`null`) → the INITIAL set: every non-deferred tool; undefined when nothing is deferred
|
|
64
|
+
* (pi's default — all active — applies, and no session entry is ever written; tool-sets without
|
|
65
|
+
* deferral behave exactly as before deferral existed).
|
|
66
|
+
*
|
|
67
|
+
* A record is NOT replayed as a frozen snapshot — the active set is rebuilt as the UNION of the
|
|
68
|
+
* initial set and the recorded names (filtered to the mounted tools: the constructor THROWS on
|
|
69
|
+
* unknown names, so a recorded-but-removed tool would otherwise brick every future invoke of that
|
|
70
|
+
* session). On the serving path only the additive activation bridge writes records, so a record's
|
|
71
|
+
* real semantic is "which deferred tools this session activated" — layered on top of whatever the
|
|
72
|
+
* workspace mounts TODAY. A snapshot replay would silently freeze a later-added non-deferred tool
|
|
73
|
+
* out of every session the loader ever touched. Missing recorded names are logged (fail visibly) —
|
|
74
|
+
* ONCE per session+missing set: a fresh harness is built per invoke and channel sessions live for
|
|
75
|
+
* weeks, so an un-deduped warn would repeat every turn and dilute its own signal. A log-dedup memo
|
|
76
|
+
* (like L2's findings memo), not session state — the resolve stays derived from the session.
|
|
77
|
+
*/
|
|
78
|
+
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
|
+
/**
|
|
95
|
+
* The session's durable override FACTS — the ONE walk both surfaces consume (`state()` reports the
|
|
96
|
+
* recorded truth; `resolveHarnessOverrides` below applies registry/scale fallbacks on top). The
|
|
97
|
+
* LAST entry of each kind wins, and a malformed record reads as ABSENT for that kind — never
|
|
98
|
+
* skipped over to an earlier record: the reporting surface and the execution surface must agree on
|
|
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.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveHarnessOverrides(entries, models, defaults, sessionId) {
|
|
131
|
+
let model = defaults.model;
|
|
132
|
+
let thinkingLevel = defaults.thinkingLevel;
|
|
133
|
+
const warnOnce = (key, message) => {
|
|
134
|
+
const emit = warnedRestores.has(key) ? log.debug : log.warn;
|
|
135
|
+
warnedRestores.add(key);
|
|
136
|
+
emit(message);
|
|
137
|
+
};
|
|
138
|
+
const recorded = lastOverrideEntries(entries);
|
|
139
|
+
if (recorded.model) {
|
|
140
|
+
const found = models.getModel(recorded.model.provider, recorded.model.modelId);
|
|
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
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (recorded.thinkingLevel !== undefined) {
|
|
148
|
+
if (THINKING_LEVELS.has(recorded.thinkingLevel)) {
|
|
149
|
+
thinkingLevel = recorded.thinkingLevel;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
warnOnce(`${sessionId}\u0000thinking\u0000${recorded.thinkingLevel}`, `[fastagent] session ${sessionId}: recorded thinking level "${recorded.thinkingLevel}" is unknown — using the configured default`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { model, thinkingLevel };
|
|
156
|
+
}
|
|
157
|
+
export function resolveHarnessActiveToolNames(recorded, tools, sessionId) {
|
|
158
|
+
const anyDeferred = tools.some(isDeferredTool);
|
|
159
|
+
const initial = tools.filter((t) => !isDeferredTool(t)).map((t) => t.name);
|
|
160
|
+
if (recorded === null)
|
|
161
|
+
return anyDeferred ? initial : undefined;
|
|
162
|
+
const mounted = new Set(tools.map((t) => t.name));
|
|
163
|
+
const known = recorded.filter((name) => mounted.has(name));
|
|
164
|
+
const missing = recorded.filter((name) => !mounted.has(name));
|
|
165
|
+
if (missing.length > 0) {
|
|
166
|
+
const emit = warnedRestores.has(`${sessionId}\u0000${missing.join(",")}`) ? log.debug : log.warn;
|
|
167
|
+
warnedRestores.add(`${sessionId}\u0000${missing.join(",")}`);
|
|
168
|
+
emit(`[fastagent] session ${sessionId}: dropping recorded activation(s) no longer mounted: ${missing.join(", ")}`);
|
|
169
|
+
}
|
|
170
|
+
return [...new Set([...initial, ...known])];
|
|
171
|
+
}
|
|
22
172
|
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing → create. */
|
|
23
173
|
export function piHarnessFactory(options) {
|
|
24
174
|
return async (sessionId) => {
|
|
25
175
|
const session = await options.sessions.openOrCreate(sessionId);
|
|
176
|
+
// 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. Serving sessions never branch, so a flat
|
|
178
|
+
// getEntries() read (no leaf-path walk) is correct.
|
|
179
|
+
const entries = await session.getEntries();
|
|
180
|
+
const activated = entries.flatMap((e) => e.type === "custom" && e.customType === TOOL_ACTIVATION_ENTRY
|
|
181
|
+
? (e.data?.names ?? [])
|
|
182
|
+
: []);
|
|
26
183
|
const fresh = options.live ? await options.live() : undefined;
|
|
27
184
|
const { systemPrompt } = options;
|
|
28
185
|
const prompt = fresh ? fresh.systemPrompt : typeof systemPrompt === "function" ? systemPrompt() : systemPrompt;
|
|
29
186
|
const skills = fresh ? fresh.skills : options.skills;
|
|
30
|
-
|
|
187
|
+
// Session overrides (set_model / set_thinking) win over the assembly defaults — same entry walk.
|
|
188
|
+
const overrides = resolveHarnessOverrides(entries, options.models, { model: options.model, thinkingLevel: options.thinkingLevel ?? DEFAULT_THINKING_LEVEL }, sessionId);
|
|
189
|
+
const harness = new AgentHarness({
|
|
31
190
|
env: options.env,
|
|
32
191
|
session,
|
|
33
192
|
models: options.models,
|
|
34
|
-
model:
|
|
193
|
+
model: overrides.model,
|
|
194
|
+
thinkingLevel: overrides.thinkingLevel,
|
|
35
195
|
tools: options.tools,
|
|
196
|
+
activeToolNames: resolveHarnessActiveToolNames(activated.length > 0 ? activated : null, options.tools ?? [], sessionId),
|
|
36
197
|
systemPrompt: prompt,
|
|
37
198
|
resources: skills ? { skills } : undefined,
|
|
38
199
|
streamOptions: { maxRetries: PROVIDER_MAX_RETRIES },
|
|
200
|
+
retry: SUMMARIZATION_RETRY_POLICY,
|
|
39
201
|
});
|
|
202
|
+
harnessSessions.set(harness, session);
|
|
203
|
+
return harness;
|
|
40
204
|
};
|
|
41
205
|
}
|