@fastagent-sh/fastagent 0.18.0 → 0.19.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 +25 -7
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +19 -0
- package/dist/atomic-write.js +48 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-service.d.ts +54 -0
- package/dist/channels/agentcore-service.js +111 -0
- package/dist/channels/agentcore.d.ts +12 -2
- package/dist/channels/agentcore.js +6 -4
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +16 -6
- package/dist/channels/control.js +106 -4
- package/dist/{engines/pi/channel.d.ts → channels/discover.d.ts} +2 -2
- package/dist/{engines/pi/channel.js → channels/discover.js} +8 -12
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.js +1 -1
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +7 -7
- package/dist/channels/feishu/invoke-turn.d.ts +1 -1
- package/dist/channels/feishu/invoke-turn.js +3 -4
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +1 -109
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +2 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/{state.js → kit/state.js} +4 -7
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +2 -2
- package/dist/channels/{tasks.js → kit/tasks.js} +1 -1
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.js → kit/turn-store.js} +1 -1
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +8 -5
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/slack-api.js +1 -1
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +8 -8
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/telegram/parse.js +2 -2
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/telegram.d.ts +1 -1
- package/dist/channels/telegram/telegram.js +3 -3
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/commands/add.js +1 -1
- package/dist/cli/commands/dev.js +25 -13
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +7 -4
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +63 -94
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +18 -57
- package/dist/cli/serve.js +57 -243
- package/dist/cli/shared.d.ts +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/preflight.js +1 -1
- package/dist/dev-supervisor.js +9 -3
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +314 -0
- package/dist/engines/pi/config.d.ts +4 -4
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +56 -43
- package/dist/engines/pi/create.js +141 -71
- package/dist/engines/pi/definition.d.ts +41 -7
- package/dist/engines/pi/definition.js +115 -11
- package/dist/engines/pi/invoke-session.d.ts +22 -19
- package/dist/engines/pi/invoke-session.js +286 -100
- package/dist/engines/pi/login.js +7 -4
- package/dist/engines/pi/models.d.ts +14 -0
- package/dist/engines/pi/models.js +9 -1
- package/dist/engines/pi/open.d.ts +9 -6
- package/dist/engines/pi/open.js +20 -10
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +19 -0
- package/dist/engines/pi/session-builder.js +84 -88
- package/dist/engines/pi/session-control.d.ts +10 -9
- package/dist/engines/pi/session-control.js +114 -77
- package/dist/engines/pi/session-inheritance.d.ts +44 -0
- package/dist/engines/pi/session-inheritance.js +242 -0
- package/dist/engines/pi/session-settings.d.ts +14 -2
- package/dist/engines/pi/session-settings.js +18 -0
- package/dist/engines/pi/session-store.d.ts +70 -0
- package/dist/engines/pi/session-store.js +313 -0
- package/dist/engines/pi/tool-context.d.ts +18 -8
- package/dist/engines/pi/tool-context.js +23 -1
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +1 -1
- package/dist/engines/pi/turn-kit.d.ts +32 -6
- package/dist/engines/pi/turn-kit.js +28 -1
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +10 -0
- package/dist/pi.d.ts +14 -8
- package/dist/pi.js +6 -5
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +1 -1
- package/dist/service.d.ts +158 -0
- package/dist/service.js +354 -0
- package/dist/session-remote.d.ts +1 -1
- package/dist/session.d.ts +4 -2
- package/dist/session.js +1 -1
- package/package.json +14 -5
- package/dist/engines/pi/harness.d.ts +0 -115
- package/dist/engines/pi/harness.js +0 -154
- package/dist/engines/pi/invoke.d.ts +0 -60
- package/dist/engines/pi/invoke.js +0 -459
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -60
- package/dist/engines/pi/sessions.js +0 -464
- package/dist/host/node.js +0 -52
- /package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +0 -0
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
- /package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
- /package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AgentSession L0's engine binding: fastagent's assembled agent — model, prompt, skills, tools —
|
|
3
|
+
* bound to one durable record, per invoke.
|
|
4
|
+
*
|
|
5
|
+
* The chat path builds the same pi class from the same assembly (session-builder.ts) but keeps ONE
|
|
6
|
+
* resident session. What is specific here is the posture: many sessions, one turn each, nothing in
|
|
7
|
+
* memory between turns.
|
|
8
|
+
*
|
|
9
|
+
* Shared once, rebuilt per turn:
|
|
10
|
+
* - `services` (ResourceLoader, settings, model runtime) is built lazily and reused — it is the
|
|
11
|
+
* expensive half, and it holds nothing session-specific;
|
|
12
|
+
* - the `AgentSession` and its tool bindings are per turn, because a tool's `execute` closes over the
|
|
13
|
+
* session it runs in and this posture has several in flight at once.
|
|
14
|
+
*/
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { createAgentSessionFromServices, createAgentSessionServices, } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { log } from "../../log.js";
|
|
18
|
+
import { isDeferredTool } from "./tool.js";
|
|
19
|
+
import { activePath, resolveSessionSettings } from "./session-settings.js";
|
|
20
|
+
import { DEFAULT_THINKING_LEVEL } from "./models.js";
|
|
21
|
+
import { additiveActivation, agentSessionManager, turnContext } from "./tool-context.js";
|
|
22
|
+
/**
|
|
23
|
+
* The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
|
|
24
|
+
* tools a loader activated in that call.
|
|
25
|
+
*
|
|
26
|
+
* A DEDICATED record, not pi's own `active_tools_change`: that one is a full SNAPSHOT of everything
|
|
27
|
+
* active at the moment, so replaying it would keep a tool active in old sessions after the author
|
|
28
|
+
* flips it to `deferred` — the session never discovered it. A delta carries only what was actually
|
|
29
|
+
* found, and is layered onto whatever the workspace mounts TODAY.
|
|
30
|
+
*/
|
|
31
|
+
const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
|
|
32
|
+
/** Every deferred tool this session has ever discovered, oldest first. */
|
|
33
|
+
function recordedActivations(session) {
|
|
34
|
+
const names = [];
|
|
35
|
+
for (const entry of session.sessionManager.getBranch()) {
|
|
36
|
+
const record = entry;
|
|
37
|
+
if (record.type !== "custom" || record.customType !== TOOL_ACTIVATION_ENTRY)
|
|
38
|
+
continue;
|
|
39
|
+
if (Array.isArray(record.data?.names)) {
|
|
40
|
+
for (const name of record.data.names)
|
|
41
|
+
if (typeof name === "string")
|
|
42
|
+
names.push(name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return names;
|
|
46
|
+
}
|
|
47
|
+
/** Warned once per session+missing set: a fresh session is built per invoke and channel sessions run
|
|
48
|
+
* for weeks, so an un-deduped warn would repeat every turn and dilute its own signal. */
|
|
49
|
+
const warnedDroppedActivations = new Set();
|
|
50
|
+
/**
|
|
51
|
+
* The turn's {@link ToolActivation} over a live session — the same bridge chat uses, so one
|
|
52
|
+
* built-in `search_tools` serves both.
|
|
53
|
+
*
|
|
54
|
+
* Activations are PERSISTED as deltas, so a tool discovered in one turn stays callable in the next.
|
|
55
|
+
* pi's own chat session does not do this (it has no place to put the record); a served session does,
|
|
56
|
+
* because the alternative is an agent that re-discovers the same capability every single turn.
|
|
57
|
+
*/
|
|
58
|
+
function sessionToolActivation(session) {
|
|
59
|
+
// Serialize activations: the read-modify-write below is only race-free while nothing awaits
|
|
60
|
+
// between read and write, and parallel tool calls in one batch would otherwise double-stamp.
|
|
61
|
+
let chain = Promise.resolve([]);
|
|
62
|
+
return {
|
|
63
|
+
active: () => session.getActiveToolNames(),
|
|
64
|
+
registered: () => session.getAllTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
|
|
65
|
+
activate(names) {
|
|
66
|
+
const run = async () => {
|
|
67
|
+
const current = session.getActiveToolNames();
|
|
68
|
+
const added = additiveActivation(session.getAllTools().map((t) => t.name), current, names);
|
|
69
|
+
if (added.length > 0) {
|
|
70
|
+
session.setActiveToolsByName([...current, ...added]);
|
|
71
|
+
session.sessionManager.appendCustomEntry(TOOL_ACTIVATION_ENTRY, { names: added });
|
|
72
|
+
}
|
|
73
|
+
return added;
|
|
74
|
+
};
|
|
75
|
+
const result = chain.then(run, run);
|
|
76
|
+
chain = result.catch(() => []);
|
|
77
|
+
return result;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* fastagent's tools as pi tool definitions, bound to ONE session.
|
|
83
|
+
*
|
|
84
|
+
* `bound` is filled after the session exists — pi needs the definitions to build the session, and a
|
|
85
|
+
* tool needs the session to reach the turn context. A tool that somehow runs before that binding
|
|
86
|
+
* throws rather than executing outside the turn: a broken lifecycle must not look like a normal
|
|
87
|
+
* out-of-turn call.
|
|
88
|
+
*/
|
|
89
|
+
function toolDefinitions(tools, cwd, env, sessionId, bound) {
|
|
90
|
+
return tools.map((tool) => ({
|
|
91
|
+
name: tool.name,
|
|
92
|
+
label: tool.name,
|
|
93
|
+
description: tool.description ?? "",
|
|
94
|
+
parameters: tool.parameters,
|
|
95
|
+
// An activating tool (the built-in loader) declares "sequential" so pi serializes its batch;
|
|
96
|
+
// without it pi's outer active-set diff double-stamps parallel calls.
|
|
97
|
+
executionMode: tool.executionMode,
|
|
98
|
+
execute: (id, params, signal) => {
|
|
99
|
+
const session = bound.session;
|
|
100
|
+
if (!session)
|
|
101
|
+
throw new Error("tool executed before its session was bound (lifecycle invariant broken)");
|
|
102
|
+
return turnContext.run({ cwd, sessionManager: agentSessionManager(session, sessionId), tools: sessionToolActivation(session) },
|
|
103
|
+
// Lower-level MountedTools may consume the fifth-argument env. Directory coding tools are
|
|
104
|
+
// cwd-bound and ignore it; authored tools read FastAgent's turnContext instead.
|
|
105
|
+
() => tool.execute(id, params, signal, undefined, { env }));
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Announce extensions pi failed to load. pi collects them into `LoadExtensionsResult.errors` and
|
|
111
|
+
* carries on with the rest — sound for a TUI that shows them, silent for a server that never looks.
|
|
112
|
+
* A definition running without the extension it ships is exactly the "quietly missing" failure this
|
|
113
|
+
* exists to remove. CHAT calls it, once per built services — serving does not load extensions at
|
|
114
|
+
* all, and announces that instead (see PiAgentSessionFactoryOptions.extensionPaths).
|
|
115
|
+
*/
|
|
116
|
+
export function reportExtensionErrors(services) {
|
|
117
|
+
for (const { path, error } of services.resourceLoader.getExtensions().errors) {
|
|
118
|
+
log.warn(`[fastagent] extension ${path} failed to load: ${error}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The resource posture a fastagent definition asks pi for — ONE definition of it, for both
|
|
123
|
+
* assemblies. Serving (`piAgentSessionFactory`) and chat (`buildAgentSessionRuntime`) build
|
|
124
|
+
* different sessions on top, but what pi is allowed to DISCOVER is not one of the differences:
|
|
125
|
+
* everything comes from the definition, nothing from the machine that happens to be running it.
|
|
126
|
+
*
|
|
127
|
+
* Two copies of this drifted once already: `additionalExtensionPaths` was added to both, and only
|
|
128
|
+
* one of them also passed the resulting tool names through pi's `tools` allowlist — so extensions
|
|
129
|
+
* worked when served and vanished in chat. A difference between the two has to be visible AS a
|
|
130
|
+
* difference, which is what the parameters are for: serving reads a prompt and skills that change
|
|
131
|
+
* per turn and passes NO extension paths (it does not run them — see
|
|
132
|
+
* {@link PiAgentSessionFactoryOptions.extensionPaths}); chat reads a fixed assembly and passes its
|
|
133
|
+
* own. Both are arguments now, rather than two files that happen to disagree.
|
|
134
|
+
*/
|
|
135
|
+
export function definitionResourceLoaderOptions(source) {
|
|
136
|
+
return {
|
|
137
|
+
// Definition-only, like dev/start: pi's machine-global discovery (the operator's own ~/.pi
|
|
138
|
+
// extensions, slash commands, global AGENTS.md, APPEND_SYSTEM.md) stays out, so the agent that
|
|
139
|
+
// runs is the artifact, not the artifact plus whoever's laptop it is.
|
|
140
|
+
noExtensions: true,
|
|
141
|
+
// ...except the definition's OWN extensions/: pi honours additionalExtensionPaths even under
|
|
142
|
+
// noExtensions, which is exactly the split wanted here — the artifact travels with its
|
|
143
|
+
// extensions, the machine's stay out.
|
|
144
|
+
...(source.extensionPaths?.length ? { additionalExtensionPaths: [...source.extensionPaths] } : {}),
|
|
145
|
+
noPromptTemplates: true,
|
|
146
|
+
noContextFiles: true,
|
|
147
|
+
// A SPACE, not "", when the assembly has no prompt: pi treats an empty custom prompt as absent
|
|
148
|
+
// and substitutes its own coding-assistant identity, which an L1 agent
|
|
149
|
+
// (`createPiAgent({ model, tools })`) never asked for. pi appends its own working-directory line
|
|
150
|
+
// either way — that is engine behaviour this binding does not fight.
|
|
151
|
+
systemPromptOverride: () => source.systemPrompt() ?? " ",
|
|
152
|
+
appendSystemPromptOverride: () => [],
|
|
153
|
+
skillsOverride: (base) => ({
|
|
154
|
+
skills: toPiSkills(source.skills()),
|
|
155
|
+
diagnostics: base.diagnostics,
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Open-or-create the record, then bind a fresh session to it. One call per invoke. */
|
|
160
|
+
export function piAgentSessionFactory(options) {
|
|
161
|
+
const { sessions, thinkingLevel, cwd, env } = options;
|
|
162
|
+
const extensionPaths = options.extensionPaths ?? [];
|
|
163
|
+
const excludedToolNames = options.excludedToolNames ?? [];
|
|
164
|
+
if (extensionPaths.length > 0) {
|
|
165
|
+
log.warn(`[fastagent] ${extensionPaths.length} extension(s) in the definition are NOT loaded when serving ` +
|
|
166
|
+
"(they run in `fastagent chat`): pi's extension runtime is shared across sessions, and serving " +
|
|
167
|
+
"runs concurrent turns for different conversations. See docs/configuration.md#extensions.");
|
|
168
|
+
}
|
|
169
|
+
const tools = options.tools ?? [];
|
|
170
|
+
const deferred = tools.filter(isDeferredTool).map((t) => t.name);
|
|
171
|
+
// What the shared ResourceLoader serves, refreshed per turn before the session is built.
|
|
172
|
+
let prompt = typeof options.systemPrompt === "function" ? options.systemPrompt() : options.systemPrompt;
|
|
173
|
+
let skills = options.skills ?? [];
|
|
174
|
+
let services;
|
|
175
|
+
let engine;
|
|
176
|
+
const buildServices = async (modelRuntime) => createAgentSessionServices({
|
|
177
|
+
cwd,
|
|
178
|
+
agentDir: options.agentDir ?? join(cwd, ".fastagent", "pi"),
|
|
179
|
+
modelRuntime,
|
|
180
|
+
// No extensionPaths: serving does not run them (see PiAgentSessionFactoryOptions), which is
|
|
181
|
+
// the one resource question the two assemblies answer differently. The accessors read the
|
|
182
|
+
// CURRENT prompt/skills — serving refreshes both per turn, so a snapshot taken here would
|
|
183
|
+
// serve a stale definition after the first edit.
|
|
184
|
+
resourceLoaderOptions: definitionResourceLoaderOptions({
|
|
185
|
+
systemPrompt: () => prompt,
|
|
186
|
+
skills: () => skills,
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
return async (sessionId, inherit) => {
|
|
190
|
+
const fresh = options.live ? await options.live() : undefined;
|
|
191
|
+
const nextPrompt = fresh
|
|
192
|
+
? fresh.systemPrompt
|
|
193
|
+
: typeof options.systemPrompt === "function"
|
|
194
|
+
? options.systemPrompt()
|
|
195
|
+
: prompt;
|
|
196
|
+
const nextSkills = fresh ? (fresh.skills ?? []) : skills;
|
|
197
|
+
engine ??= options.engine();
|
|
198
|
+
const { modelRuntime, model } = await engine;
|
|
199
|
+
if (services === undefined) {
|
|
200
|
+
prompt = nextPrompt;
|
|
201
|
+
skills = nextSkills;
|
|
202
|
+
services = buildServices(modelRuntime); // assigned before any await: concurrent turns share it
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// The ResourceLoader reads the overrides once and caches, so a re-read of the definition only
|
|
206
|
+
// reaches the model after a reload. Reload only when the definition ACTUALLY changed — an
|
|
207
|
+
// author edits persona.md far less often than the agent takes a turn, and a reload costs ~5ms
|
|
208
|
+
// against ~0.6ms to bind a session.
|
|
209
|
+
//
|
|
210
|
+
// "Changed" is measured against the LOADER, not against what this factory last wrote. Serving
|
|
211
|
+
// is concurrent across sessions, and a shared variable makes the check lie: one turn writes
|
|
212
|
+
// its new prompt, awaits before reloading, and the next turn sees that value already present,
|
|
213
|
+
// concludes nothing changed, and skips the reload — so the edit reaches neither the loader nor
|
|
214
|
+
// any error. Asking the loader what it is actually serving cannot go stale that way. The cost
|
|
215
|
+
// of losing the race is one redundant reload, not a swallowed edit.
|
|
216
|
+
//
|
|
217
|
+
// Skill CONTENT is not part of this — pi reads a skill from its file at invocation time, so
|
|
218
|
+
// only the declared set matters.
|
|
219
|
+
//
|
|
220
|
+
// What this deliberately does NOT provide is a per-turn snapshot. The definition is an AGENT
|
|
221
|
+
// property, not a session one: two turns running either side of an edit each get a definition
|
|
222
|
+
// that genuinely existed, and the product promise — an edit is live on the next turn — holds
|
|
223
|
+
// for both. Pinning a snapshot per turn would cost either a loader per turn or a queue in
|
|
224
|
+
// front of every bind, to buy a guarantee nothing asks for.
|
|
225
|
+
const loader = (await services).resourceLoader;
|
|
226
|
+
const definitionChanged = loader.getSystemPrompt() !== nextPrompt || loadedSkillSet(loader.getSkills().skills) !== skillSet(nextSkills);
|
|
227
|
+
if (definitionChanged) {
|
|
228
|
+
prompt = nextPrompt;
|
|
229
|
+
skills = nextSkills;
|
|
230
|
+
await loader.reload();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const sessionManager = await sessions.openOrCreate(sessionId, inherit);
|
|
234
|
+
// What the session RUNS on: the boundary plane records model/thinking overrides as entries, and
|
|
235
|
+
// pi does not read them back — a binding that ignored them would silently run every turn on the
|
|
236
|
+
// assembly default, and `state()` would report a setting no turn uses.
|
|
237
|
+
// The SAME read the control plane performs, including its integrity check: a record whose chain
|
|
238
|
+
// is broken must not run on assembly defaults while `state()` rejects it — one of the two planes
|
|
239
|
+
// would be lying. A throw here becomes this turn's `failed` event, which is where the fault has
|
|
240
|
+
// a channel to be reported through.
|
|
241
|
+
const settings = resolveSessionSettings(activePath(sessionManager), modelRuntime, {
|
|
242
|
+
model,
|
|
243
|
+
thinkingLevel: thinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
244
|
+
});
|
|
245
|
+
const bound = {};
|
|
246
|
+
const { session } = await createAgentSessionFromServices({
|
|
247
|
+
services: await services,
|
|
248
|
+
sessionManager,
|
|
249
|
+
model: settings.model,
|
|
250
|
+
thinkingLevel: settings.thinkingLevel,
|
|
251
|
+
// pi would otherwise mount its built-ins on top of fastagent's copies, offering duplicate names.
|
|
252
|
+
// Lower-level callers with an explicit list also rely on omitted built-ins staying omitted.
|
|
253
|
+
noTools: "builtin",
|
|
254
|
+
...(excludedToolNames.length > 0 ? { excludeTools: [...excludedToolNames] } : {}),
|
|
255
|
+
customTools: toolDefinitions(tools, cwd, env, sessionId, bound),
|
|
256
|
+
});
|
|
257
|
+
bound.session = session;
|
|
258
|
+
// An extension handler that throws is otherwise dropped: pi fans errors out to registered
|
|
259
|
+
// listeners and has none by default, which on a server means a broken extension looks like an
|
|
260
|
+
// extension that simply did nothing.
|
|
261
|
+
//
|
|
262
|
+
// Deferral, then restoration: pi starts every mounted tool active, so narrow by SUBTRACTING the
|
|
263
|
+
// deferred names (robust to pi mounting tools of its own, unlike an exact-set replacement), then
|
|
264
|
+
// add back what THIS session has already discovered.
|
|
265
|
+
if (deferred.length > 0) {
|
|
266
|
+
const active = session.getActiveToolNames();
|
|
267
|
+
const mounted = new Set(session.getAllTools().map((tool) => tool.name));
|
|
268
|
+
const recorded = recordedActivations(session);
|
|
269
|
+
// A recorded name that is no longer mounted is dropped rather than replayed: pi's setter
|
|
270
|
+
// THROWS on an unknown name, so replaying one would brick every future turn of this session.
|
|
271
|
+
const restored = recorded.filter((name) => mounted.has(name));
|
|
272
|
+
const dropped = recorded.filter((name) => !mounted.has(name));
|
|
273
|
+
if (dropped.length > 0) {
|
|
274
|
+
const key = `${sessionId}\u0000${[...new Set(dropped)].sort().join(",")}`;
|
|
275
|
+
const emit = warnedDroppedActivations.has(key) ? log.debug : log.warn;
|
|
276
|
+
warnedDroppedActivations.add(key);
|
|
277
|
+
emit(`[fastagent] session ${sessionId}: dropping recorded activation(s) no longer mounted: ${[...new Set(dropped)].join(", ")}`);
|
|
278
|
+
}
|
|
279
|
+
const next = [...new Set([...active.filter((name) => !deferred.includes(name)), ...restored])];
|
|
280
|
+
if (next.length !== active.length || next.some((name) => !active.includes(name))) {
|
|
281
|
+
session.setActiveToolsByName(next);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return session;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** What a reload has to notice: the declared set, not the files behind it. */
|
|
288
|
+
function skillSet(skills) {
|
|
289
|
+
return skills.map((s) => `${s.name}\u0000${s.filePath}\u0000${s.description}`).join("\u0001");
|
|
290
|
+
}
|
|
291
|
+
/** The same signature, read back off the loader. */
|
|
292
|
+
function loadedSkillSet(skills) {
|
|
293
|
+
return skills.map((s) => `${s.name}\u0000${s.filePath ?? ""}\u0000${s.description}`).join("\u0001");
|
|
294
|
+
}
|
|
295
|
+
/** fastagent's Skill (content inline) as pi's (read from filePath at invocation time). */
|
|
296
|
+
function toPiSkills(skills) {
|
|
297
|
+
return skills.map((skill) => {
|
|
298
|
+
const baseDir = dirname(skill.filePath);
|
|
299
|
+
return {
|
|
300
|
+
name: skill.name,
|
|
301
|
+
description: skill.description,
|
|
302
|
+
filePath: skill.filePath,
|
|
303
|
+
baseDir,
|
|
304
|
+
sourceInfo: {
|
|
305
|
+
path: skill.filePath,
|
|
306
|
+
source: "fastagent",
|
|
307
|
+
scope: "project",
|
|
308
|
+
origin: "top-level",
|
|
309
|
+
baseDir,
|
|
310
|
+
},
|
|
311
|
+
disableModelInvocation: skill.disableModelInvocation ?? false,
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { FastagentTool } from "./tool.ts";
|
|
3
3
|
import type { Models } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { AnyModel } from "./
|
|
4
|
+
import type { AnyModel } from "./models.ts";
|
|
5
5
|
export interface FastagentConfig {
|
|
6
6
|
/** "provider/modelId". Precedence: CLI --model > FASTAGENT_MODEL > config. */
|
|
7
7
|
model?: string;
|
|
@@ -9,8 +9,8 @@ export interface FastagentConfig {
|
|
|
9
9
|
* "xhigh" | "max"). Unset = pi's default. Authors tune thinking in the pi TUI while vibing — this
|
|
10
10
|
* is the serving-side counterpart (fidelity). Levels a model doesn't support are clamped by pi. */
|
|
11
11
|
thinkingLevel?: ThinkingLevel;
|
|
12
|
-
/** Extra custom tools, appended after pi
|
|
13
|
-
* plus the optional `deferred` marker (see defineTool). */
|
|
12
|
+
/** Extra custom tools, appended after the pi coding tools — never replaces them. `FastagentTool`
|
|
13
|
+
* = AgentTool plus the optional `deferred` marker (see defineTool). */
|
|
14
14
|
tools?: FastagentTool[];
|
|
15
15
|
/** `host` is the bind address: unset (or `0.0.0.0`) binds all interfaces — what containers need;
|
|
16
16
|
* `127.0.0.1` keeps the serve (including `/control/*`) off the LAN. Precedence: `--bind` > this. */
|
|
@@ -60,7 +60,7 @@ export declare function loadConfig(dir: string): Promise<LoadedConfig>;
|
|
|
60
60
|
* lookups then miss visibly (an unknown-provider error / a login-required hint), never a mangled id
|
|
61
61
|
* (`slice(0, indexOf("/"))` silently drops the last char when "/" is absent). */
|
|
62
62
|
export declare function providerOf(spec: string): string;
|
|
63
|
-
/** Resolve "provider/modelId" → a pi Model from `models`, so
|
|
63
|
+
/** Resolve "provider/modelId" → a pi Model from `models`, so auth resolves from the same collection. */
|
|
64
64
|
export declare function resolveModel(models: Models, spec: string): AnyModel;
|
|
65
65
|
/** All registered "provider/modelId" specs in `models`, sorted — the list behind `fastagent models`. */
|
|
66
66
|
export declare function listModels(models: Models): string[];
|
|
@@ -151,7 +151,7 @@ export function providerOf(spec) {
|
|
|
151
151
|
const slash = spec.indexOf("/");
|
|
152
152
|
return slash > 0 ? spec.slice(0, slash) : spec;
|
|
153
153
|
}
|
|
154
|
-
/** Resolve "provider/modelId" → a pi Model from `models`, so
|
|
154
|
+
/** Resolve "provider/modelId" → a pi Model from `models`, so auth resolves from the same collection. */
|
|
155
155
|
export function resolveModel(models, spec) {
|
|
156
156
|
const slash = spec.indexOf("/");
|
|
157
157
|
if (slash < 1 || slash === spec.length - 1) {
|
|
@@ -1,25 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import type { ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { 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 { type AnyModel, piHarnessFactory } from "./harness.ts";
|
|
7
|
-
import { type PiSessionStore } from "./sessions.ts";
|
|
8
6
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
9
7
|
import { type ToolCollision, type MountedTool } from "./tool.ts";
|
|
10
|
-
import { type
|
|
11
|
-
import { type
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
|
|
16
|
-
export declare function resolveTools(config: FastagentConfig): MountedTool[];
|
|
8
|
+
import { type PiAgentSessionFactory } from "./invoke-session.ts";
|
|
9
|
+
import { type AnyModel } from "./models.ts";
|
|
10
|
+
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { type PiSessionRecordStore } from "./session-store.ts";
|
|
12
|
+
import { type Lease, type SessionObserver } from "./turn-kit.ts";
|
|
17
13
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* Every pi coding tool, in canonical order, rooted at the workspace it operates in.
|
|
15
|
+
*
|
|
16
|
+
* pi ships two overlapping groupings and neither is the whole set: `createCodingTools` is the four it
|
|
17
|
+
* ACTIVATES for a terminal (read/bash/edit/write, no searching), `createReadOnlyTools` is
|
|
18
|
+
* read/grep/find/ls. `read` is in both; a directory agent mounts their union.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CODING_TOOL_NAMES: readonly ["read", "grep", "find", "ls", "bash", "edit", "write"];
|
|
21
|
+
export declare function piAllCodingTools(cwd: string): MountedTool[];
|
|
22
|
+
/**
|
|
23
|
+
* The full directory-agent tool set: all pi coding tools + `config.tools` + discovered `tools/`
|
|
24
|
+
* (deduped, existing win), plus the authored names and collisions to report. One source for the
|
|
20
25
|
* dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
|
|
21
26
|
*/
|
|
22
|
-
export declare function resolveAgentTools(config: FastagentConfig, agentDir: string): Promise<{
|
|
27
|
+
export declare function resolveAgentTools(config: FastagentConfig, agentDir: string, cwd: string): Promise<{
|
|
23
28
|
tools: MountedTool[];
|
|
24
29
|
toolNames: string[];
|
|
25
30
|
/** Tools registered but not initially active (defineTool `deferred: true`) — discovered/activated
|
|
@@ -57,20 +62,22 @@ export interface AssembleSystemPromptOptions {
|
|
|
57
62
|
export declare function assembleSystemPrompt(options: AssembleSystemPromptOptions): string;
|
|
58
63
|
/**
|
|
59
64
|
* INTERNAL seam (workspace ↔ assembly): hands the hub-wiring consumer the assembly's live parts —
|
|
60
|
-
* the SAME
|
|
61
|
-
* mutations (session-control.ts) contend on the real lease and validate against the real
|
|
62
|
-
* Called synchronously, exactly once, before the agent is returned. Not
|
|
65
|
+
* the SAME session factory and lease the agent runs with, plus the model registry behind a thunk —
|
|
66
|
+
* so boundary mutations (session-control.ts) contend on the real lease and validate against the real
|
|
67
|
+
* registry. Called synchronously, exactly once, before the agent is returned. Not public surface.
|
|
63
68
|
*/
|
|
64
|
-
type
|
|
65
|
-
models: Models;
|
|
66
|
-
harnessFactory: ReturnType<typeof piHarnessFactory>;
|
|
69
|
+
export type PiAssemblyParts = {
|
|
67
70
|
lease: Lease;
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
sessionFactory: PiAgentSessionFactory;
|
|
72
|
+
/** The registry and configured model, resolved on first use (a credential read is async). */
|
|
73
|
+
engine: () => Promise<{
|
|
74
|
+
modelRuntime: ModelRuntime;
|
|
70
75
|
model: AnyModel;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
}>;
|
|
77
|
+
/** The configured reasoning effort — the other half of the pair a session without overrides runs on. */
|
|
78
|
+
thinkingLevel: ThinkingLevel;
|
|
79
|
+
};
|
|
80
|
+
type OnAssembly = (parts: PiAssemblyParts) => void;
|
|
74
81
|
/** L1 options. Tier 1: model (spec) + instructions + tools. Tier 2: the injectable ports. */
|
|
75
82
|
export interface CreatePiAgentOptions {
|
|
76
83
|
/** Model spec "provider/modelId" (e.g. "openai-codex/gpt-5.5"), resolved against {@link models}. */
|
|
@@ -78,14 +85,18 @@ export interface CreatePiAgentOptions {
|
|
|
78
85
|
/** Reasoning effort (pi's scale). Unset = pi's default; unsupported levels are clamped per model. */
|
|
79
86
|
thinkingLevel?: ThinkingLevel;
|
|
80
87
|
/**
|
|
81
|
-
* The system prompt itself —
|
|
82
|
-
*
|
|
83
|
-
*
|
|
88
|
+
* The system prompt itself — no engine base and no wrapping (unlike the directory path, which
|
|
89
|
+
* assembles the engine base + AGENTS.md as segment ② + persona.md as segment ①). A plain string or
|
|
90
|
+
* a factory re-evaluated per invoke. When {@link skills} are mounted their listing is appended.
|
|
91
|
+
*
|
|
92
|
+
* Not byte-for-byte verbatim: pi appends its own `Current working directory:` line to whatever
|
|
93
|
+
* prompt it is given. What this rung guarantees is that no engine IDENTITY is imposed — a
|
|
94
|
+
* hand-built agent is not told it is a coding assistant.
|
|
84
95
|
*/
|
|
85
96
|
instructions?: string | (() => string);
|
|
86
97
|
/** The tool set to mount. `FastagentTool` (AgentTool plus the optional `deferred` marker, see
|
|
87
98
|
* {@link DefineToolOptions}) widens into {@link MountedTool}, which additionally admits pi's default
|
|
88
|
-
* coding tools —
|
|
99
|
+
* coding tools — those bind their workspace at construction rather than reading a turn context. */
|
|
89
100
|
tools?: MountedTool[];
|
|
90
101
|
skills?: Skill[];
|
|
91
102
|
/**
|
|
@@ -100,12 +111,15 @@ export interface CreatePiAgentOptions {
|
|
|
100
111
|
* consulted when a provider is absent from the file (resolution order is upstream-owned).
|
|
101
112
|
*/
|
|
102
113
|
authPath?: string;
|
|
103
|
-
/** Session persistence. Defaults to in-memory; inject
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
* NOT constrain
|
|
114
|
+
/** Session persistence. Defaults to in-memory; inject piSessionRecordStore for restart-surviving
|
|
115
|
+
* continuity. */
|
|
116
|
+
sessions?: PiSessionRecordStore;
|
|
117
|
+
/** Filesystem/process environment, handed to tools that read one as the turn's context. Defaults to
|
|
118
|
+
* a local NodeExecutionEnv at `process.cwd()`. At THIS rung nothing else consumes it: L1 loads no
|
|
119
|
+
* definition. It does NOT constrain the coding tools (pi's own, rooted at the workspace they were
|
|
120
|
+
* built for) or author-written `tools/`, which are code and can import anything. Not a
|
|
121
|
+
* sandbox — see {@link createPiAgentFromDefinition} for the rung where it also reads the
|
|
122
|
+
* definition. */
|
|
109
123
|
env?: ExecutionEnv;
|
|
110
124
|
/** Single-writer lease. Defaults to in-process fail-fast inProcessLease(). */
|
|
111
125
|
lease?: Lease;
|
|
@@ -127,11 +141,11 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
127
141
|
/** Override the engine base prompt (segment ①). Defaults to piBasePrompt({ tools, persona }) using the
|
|
128
142
|
* live-read persona.md; pass base to fully opt out of persona.md. */
|
|
129
143
|
base?: string;
|
|
130
|
-
/** Override tools. Defaults to {@link
|
|
144
|
+
/** Override tools. Defaults to {@link piAllCodingTools} (lock down with a custom list). An authored
|
|
131
145
|
* `FastagentTool[]` (AgentTool plus the optional `deferred` marker) widens into {@link MountedTool}. */
|
|
132
146
|
tools?: MountedTool[];
|
|
133
147
|
/**
|
|
134
|
-
* The agent's working directory: where the
|
|
148
|
+
* The agent's working directory: where the coding tools operate AND whose ancestors are walked for
|
|
135
149
|
* ② project context (AGENTS.md). Defaults to `dir`. Set it to the enclosing repo so a coding agent
|
|
136
150
|
* whose definition lives in `dir` operates on — and reads the AGENTS.md of — that repo (core.md
|
|
137
151
|
* scenario grid); that is what the CLI's opener does with the workspace.
|
|
@@ -145,12 +159,11 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
145
159
|
* dir) — unlike the dir-less {@link createPiAgent}/{@link createPiModels}, which default global.
|
|
146
160
|
*/
|
|
147
161
|
authPath?: string;
|
|
148
|
-
sessions?:
|
|
149
|
-
/** Filesystem/process environment; see {@link CreatePiAgentOptions.env}. At THIS rung it
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* env narrows the blast radius rather than closing it. */
|
|
162
|
+
sessions?: PiSessionRecordStore;
|
|
163
|
+
/** Filesystem/process environment; see {@link CreatePiAgentOptions.env}. At THIS rung it reads
|
|
164
|
+
* persona.md and skills/. The seven coding tools, ② project context (pi's
|
|
165
|
+
* loadProjectContextFiles uses node fs directly), and author-written `tools/` stay outside it.
|
|
166
|
+
* Injecting an env narrows the blast radius rather than closing it. */
|
|
154
167
|
env?: ExecutionEnv;
|
|
155
168
|
lease?: Lease;
|
|
156
169
|
/** Observation-plane tap; see {@link CreatePiAgentOptions.observer}. */
|