@fastagent-sh/fastagent 0.19.0 → 0.21.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 -1
- package/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +12 -15
- package/dist/channels/agentcore-service.js +15 -24
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +9 -83
- package/dist/channels/agentcore.js +101 -93
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +251 -157
- package/dist/channels/discover.d.ts +26 -5
- package/dist/channels/discover.js +13 -26
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +7 -12
- package/dist/channels/feishu/feishu.js +86 -128
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +44 -23
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +35 -15
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -141
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +100 -73
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +68 -107
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +2 -2
- package/dist/channels/telegram/telegram.js +97 -204
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +13 -25
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -35
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +38 -25
- package/dist/cli/commands/deploy.js +101 -631
- package/dist/cli/commands/dev.js +20 -76
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +41 -80
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +42 -34
- package/dist/cli/serve.js +107 -28
- package/dist/cli/shared.d.ts +52 -27
- package/dist/cli/shared.js +69 -6
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +34 -11
- package/dist/deploy/agentcore/plan.js +52 -259
- package/dist/deploy/agentcore/run.d.ts +9 -24
- package/dist/deploy/agentcore/run.js +35 -43
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +35 -4
- package/dist/deploy/docker/run.js +40 -8
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +14 -22
- package/dist/deploy/fly/run.d.ts +36 -23
- package/dist/deploy/fly/run.js +129 -83
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +9 -5
- package/dist/deploy/railway/plan.js +18 -26
- package/dist/deploy/railway/run.d.ts +11 -10
- package/dist/deploy/railway/run.js +44 -43
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +63 -28
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +111 -106
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +2 -2
- package/dist/engines/pi/create.d.ts +22 -27
- package/dist/engines/pi/create.js +59 -74
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +11 -12
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +3 -4
- package/dist/engines/pi/open.js +32 -42
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +26 -140
- package/dist/engines/pi/session-control.d.ts +27 -22
- package/dist/engines/pi/session-control.js +551 -490
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +5 -5
- package/dist/engines/pi/session-settings.js +8 -5
- package/dist/engines/pi/session-store.d.ts +91 -26
- package/dist/engines/pi/session-store.js +413 -82
- package/dist/engines/pi/tool-context.d.ts +24 -11
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +3 -2
- package/dist/env.js +1 -2
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +51 -7
- package/dist/loader.js +84 -18
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +26 -3
- package/dist/paths.js +43 -5
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +50 -67
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +14 -19
- package/dist/schedule/wake-alarm.js +89 -48
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +24 -24
- package/dist/service.js +33 -77
- package/dist/session-remote.d.ts +12 -7
- package/dist/session-remote.js +185 -118
- package/dist/session.d.ts +227 -93
- package/dist/session.js +61 -23
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +34 -9
- package/dist/tunnel.js +83 -50
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
|
@@ -9,15 +9,20 @@
|
|
|
9
9
|
* Above L2 sits the agent opener createPiAgentFromDir (open.ts), which both `dev` and
|
|
10
10
|
* `start` drive. Each rung calls the one below; options narrow as you go up (L2 owns systemPrompt/skills —
|
|
11
11
|
* they come from the definition; the openers own model/tools — from config resolution).
|
|
12
|
+
*
|
|
13
|
+
* Every rung assembles the same VALUE first — a {@link PiAssembly}: the lease, the session factory
|
|
14
|
+
* and the engine thunk — and the agent is that value under the L0. The opener needs the value
|
|
15
|
+
* itself (the control plane contends on the same lease and validates against the same registry), so
|
|
16
|
+
* `assemblePiFromDefinition` hands it out and `createPiAgentFromDefinition` is it plus the L0.
|
|
12
17
|
*/
|
|
13
|
-
import {
|
|
18
|
+
import { toUSVString } from "node:util";
|
|
14
19
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
15
|
-
import { createCodingTools, createPowerShellTool, createReadOnlyTools } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { createCodingTools, createPowerShellTool, createReadOnlyTools, } from "@earendil-works/pi-coding-agent";
|
|
16
21
|
import { defaultAuthPath, resolveModel } from "./config.js";
|
|
17
22
|
import { resolveSecretsDir } from "../../paths.js";
|
|
18
23
|
import { loadAgentDefinition, loadExtensionPaths } from "./definition.js";
|
|
19
24
|
import { reportFindingsIfChanged } from "./report.js";
|
|
20
|
-
import { isDeferredTool, loadTools, mergeDiscoveredTools
|
|
25
|
+
import { isDeferredTool, loadTools, mergeDiscoveredTools } from "./tool.js";
|
|
21
26
|
import { withSearchTool } from "./search-tools.js";
|
|
22
27
|
import { createPiAgentFromSession } from "./invoke-session.js";
|
|
23
28
|
import { piAgentSessionFactory } from "./agent-session-factory.js";
|
|
@@ -50,7 +55,21 @@ import { inProcessLease } from "./turn-kit.js";
|
|
|
50
55
|
*/
|
|
51
56
|
export const CODING_TOOL_NAMES = ["read", "grep", "find", "ls", "bash", "edit", "write"];
|
|
52
57
|
export function piAllCodingTools(cwd) {
|
|
53
|
-
const mutating = createCodingTools(cwd
|
|
58
|
+
const mutating = createCodingTools(cwd, {
|
|
59
|
+
bash: {
|
|
60
|
+
spawnHook(context) {
|
|
61
|
+
const { env } = context;
|
|
62
|
+
const id = env.PI_SESSION_ID;
|
|
63
|
+
delete env.PI_SESSION_ID_ENCODING;
|
|
64
|
+
// OS environment strings cannot preserve NUL or unpaired UTF-16 surrogates.
|
|
65
|
+
if (id !== undefined && (id.includes("\u0000") || toUSVString(id) !== id)) {
|
|
66
|
+
env.PI_SESSION_ID = JSON.stringify(id);
|
|
67
|
+
env.PI_SESSION_ID_ENCODING = "json";
|
|
68
|
+
}
|
|
69
|
+
return context;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}).filter((tool) => tool.name !== "read");
|
|
54
73
|
return [...createReadOnlyTools(cwd), ...mutating];
|
|
55
74
|
}
|
|
56
75
|
/**
|
|
@@ -119,18 +138,7 @@ export async function resolveAgentTools(config, agentDir, cwd) {
|
|
|
119
138
|
toolFailures: discovered.failures,
|
|
120
139
|
};
|
|
121
140
|
}
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
// systemPrompt = ① base (engine asset; a persona.md persona overrides its identity line)
|
|
125
|
-
// + ② project context (AGENTS.md files via pi's loadProjectContextFiles, <project_context>-wrapped)
|
|
126
|
-
// + ③ skills listing + ④ env context (cwd)
|
|
127
|
-
//
|
|
128
|
-
// AGENTS.md ≠ system prompt. Pure functions: segment ④ input (cwd) is caller-provided, so the
|
|
129
|
-
// same inputs always produce the same prompt (testable, reproducible). No date: a date line would
|
|
130
|
-
// invalidate the provider prompt cache (a prefix cache) for every session at each day boundary —
|
|
131
|
-
// channel sessions routinely live for weeks (pi ≥0.80.7 dropped it from its default prompt for the
|
|
132
|
-
// same reason). The model gets the date when it needs it: `bash date`, and the wake tool takes
|
|
133
|
-
// relative delays ("30m") / cron — never an absolute now-derived instant.
|
|
141
|
+
// Fastagent owns identity and project context; Pi appends skills and cwd for both serving and chat.
|
|
134
142
|
/**
|
|
135
143
|
* The pi engine's base prompt (segment ①), mirroring pi-coding-agent's default path with two
|
|
136
144
|
* deviations: the pi-TUI docs section is dropped (those paths don't exist in deployments), and the
|
|
@@ -147,7 +155,7 @@ export function piBasePrompt(options = {}) {
|
|
|
147
155
|
const deferredCount = mounted.length - tools.length;
|
|
148
156
|
const toolsList = tools.length > 0 ? tools.map((t) => `- ${t.name}: ${(t.description ?? "").split("\n")[0]}`).join("\n") : "(none)";
|
|
149
157
|
// Segment ① identity: an authored persona (persona.md) replaces the default engine identity line
|
|
150
|
-
// (core.md §
|
|
158
|
+
// (core.md §2), keeping the tools list + guidelines below. Preserve pi's coding identity only for
|
|
151
159
|
// the full coding surface; a partial/empty surface must not claim machine capabilities it lacks.
|
|
152
160
|
// The four this sentence NAMES — reading, executing, editing, writing. Searching is not part of the
|
|
153
161
|
// claim, so requiring it would demote an agent that can do everything the identity says it can.
|
|
@@ -182,30 +190,23 @@ export function assembleSystemPrompt(options) {
|
|
|
182
190
|
}
|
|
183
191
|
prompt += `</project_context>\n`;
|
|
184
192
|
}
|
|
185
|
-
if (options.skills && options.skills.length > 0) {
|
|
186
|
-
prompt += `\n${formatSkillsForSystemPrompt(options.skills)}\n`;
|
|
187
|
-
}
|
|
188
|
-
if (options.cwd)
|
|
189
|
-
prompt += `\nCurrent working directory: ${options.cwd}`;
|
|
190
193
|
return prompt;
|
|
191
194
|
}
|
|
192
|
-
// ── §3 the reusable assembly ladder: L1 / L2 ────────────────────────────────
|
|
193
195
|
/**
|
|
194
196
|
* Shared low-level wiring: resolve the model spec against the collection, default the K ports, build
|
|
195
|
-
* the
|
|
197
|
+
* the parts. Internal — the public rungs decide the systemPrompt (L1 from instructions, L2 from the
|
|
196
198
|
* directory) and route through here.
|
|
197
199
|
*/
|
|
198
|
-
function
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
// instance the agent runs under — boundary mutations must contend on it.
|
|
200
|
+
function assemblePi(opts) {
|
|
201
|
+
const cwd = opts.cwd ?? opts.env?.cwd ?? process.cwd();
|
|
202
|
+
// Materialized here (not defaulted inside the L0) so the value carries the SAME lease instance the
|
|
203
|
+
// agent runs under — boundary mutations must contend on it.
|
|
203
204
|
const lease = opts.lease ?? inProcessLease();
|
|
204
205
|
const sessions = opts.sessions ?? piInMemorySessionRecordStore({ cwd });
|
|
205
206
|
// The model and its runtime resolve on FIRST USE: building a ModelRuntime is async while
|
|
206
207
|
// assembling an agent is not, so the credential read belongs on the first turn rather than in the
|
|
207
|
-
// caller's constructor. Memoized
|
|
208
|
-
//
|
|
208
|
+
// caller's constructor. Memoized, and shared with the control plane so a boundary mutation
|
|
209
|
+
// validates against the registry the runs actually use.
|
|
209
210
|
let engine;
|
|
210
211
|
const resolveEngine = () => {
|
|
211
212
|
engine ??= (async () => {
|
|
@@ -225,71 +226,56 @@ function buildPiAgent(opts) {
|
|
|
225
226
|
engine: resolveEngine,
|
|
226
227
|
thinkingLevel: opts.thinkingLevel,
|
|
227
228
|
tools: opts.tools,
|
|
228
|
-
|
|
229
|
-
skills: opts.skills,
|
|
230
|
-
live: opts.live,
|
|
229
|
+
readDefinition: opts.readDefinition,
|
|
231
230
|
cwd,
|
|
232
231
|
...(opts.agentDir ? { agentDir: opts.agentDir } : {}),
|
|
233
232
|
...(opts.extensionPaths ? { extensionPaths: opts.extensionPaths } : {}),
|
|
234
233
|
// `noTools: "builtin"` leaves pi's built-ins in the registry; a lower-level replacement must also
|
|
235
234
|
// deny every omitted coding name so a loader cannot reactivate one later.
|
|
236
235
|
excludedToolNames: omittedBuiltinNames(opts.tools ?? [], cwd),
|
|
237
|
-
env,
|
|
238
236
|
});
|
|
239
|
-
|
|
237
|
+
return {
|
|
240
238
|
lease,
|
|
241
239
|
sessionFactory,
|
|
242
|
-
// The control plane needs the registry and the configured pair as VALUES, and both only exist
|
|
243
|
-
// after the first credential read. Asking for them lazily keeps assembly synchronous without
|
|
244
|
-
// making the hub wait on a runtime it may never need (a control-less deployment never calls it).
|
|
245
240
|
engine: resolveEngine,
|
|
246
241
|
thinkingLevel: opts.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
247
|
-
});
|
|
248
|
-
return createPiAgentFromSession({ lease, observer: opts.observer, sessionFactory });
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* L1 system prompt: `instructions` ARE the prompt (no engine base, no wrapping); the skills listing
|
|
252
|
-
* is appended only when skills are mounted (the model must know what it can invoke). A factory so a
|
|
253
|
-
* dynamic `instructions` and per-invoke freshness both work; undefined when there is nothing to send.
|
|
254
|
-
*/
|
|
255
|
-
function instructionsPrompt(instructions, skills) {
|
|
256
|
-
const hasSkills = skills !== undefined && skills.length > 0;
|
|
257
|
-
if (instructions === undefined && !hasSkills)
|
|
258
|
-
return undefined;
|
|
259
|
-
return () => {
|
|
260
|
-
const prose = typeof instructions === "function" ? instructions() : (instructions ?? "");
|
|
261
|
-
const listing = hasSkills ? formatSkillsForSystemPrompt(skills) : "";
|
|
262
|
-
return [prose, listing].filter((s) => s !== "").join("\n");
|
|
263
242
|
};
|
|
264
243
|
}
|
|
244
|
+
/** The agent an assembly runs as: the L0 over its lease and session factory. */
|
|
245
|
+
export function agentOf(assembly, observer) {
|
|
246
|
+
return createPiAgentFromSession({ lease: assembly.lease, observer, sessionFactory: assembly.sessionFactory });
|
|
247
|
+
}
|
|
265
248
|
/** L1: assemble from typed parts. */
|
|
266
249
|
export function createPiAgent(options) {
|
|
267
|
-
|
|
250
|
+
const { instructions, skills = [] } = options;
|
|
251
|
+
return agentOf(assemblePi({
|
|
268
252
|
model: options.model,
|
|
269
253
|
thinkingLevel: options.thinkingLevel,
|
|
270
254
|
providers: options.providers,
|
|
271
255
|
authPath: options.authPath,
|
|
272
|
-
|
|
256
|
+
readDefinition: () => ({
|
|
257
|
+
systemPrompt: typeof instructions === "function" ? instructions() : instructions,
|
|
258
|
+
skills,
|
|
259
|
+
}),
|
|
273
260
|
// Deferred tools need their loader on every rung (idempotent; the caller's own search_tools wins).
|
|
274
261
|
tools: options.tools ? withSearchTool(options.tools) : options.tools,
|
|
275
|
-
skills: options.skills,
|
|
276
262
|
sessions: options.sessions,
|
|
277
263
|
env: options.env,
|
|
278
264
|
lease: options.lease,
|
|
279
|
-
|
|
280
|
-
});
|
|
265
|
+
}), options.observer);
|
|
281
266
|
}
|
|
282
267
|
/**
|
|
283
|
-
* L2
|
|
284
|
-
*
|
|
268
|
+
* L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. Returns the
|
|
269
|
+
* definition so callers can surface diagnostics/collisions. The opener consumes this form because
|
|
270
|
+
* the control plane needs the parts; {@link createPiAgentFromDefinition} is it plus the L0.
|
|
285
271
|
*/
|
|
286
|
-
export async function
|
|
272
|
+
export async function assemblePiFromDefinition(dir, options) {
|
|
287
273
|
// `dir` = the agent-definition dir (persona.md/skills/); `cwd` (default = dir) is the run root where
|
|
288
274
|
// tools operate and whose ancestors are walked for ② context.
|
|
289
275
|
const cwd = options.cwd ?? dir;
|
|
290
276
|
const env = options.env ?? new NodeExecutionEnv({ cwd });
|
|
291
277
|
// Boot-time load: fail-visibly at startup on a broken directory, and give callers the snapshot to
|
|
292
|
-
// report (skills/diagnostics/collisions). Serving
|
|
278
|
+
// report (skills/diagnostics/collisions). Serving re-reads it through readDefinition below.
|
|
293
279
|
// `cwd`, not `env.cwd`, for the same reason as the tools below: `cwd` is the run root whose
|
|
294
280
|
// ancestors carry ② project context. Reading it off the env pointed the AGENTS.md walk at the
|
|
295
281
|
// loader's directory whenever a caller supplied both.
|
|
@@ -308,7 +294,7 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
308
294
|
// Dir-aware default: the same secrets-dir-derived file the opener uses for this dir (the opener
|
|
309
295
|
// passes an explicit authPath, so this only affects direct L2 callers).
|
|
310
296
|
const authPath = options.authPath ?? defaultAuthPath(resolveSecretsDir(dir));
|
|
311
|
-
const
|
|
297
|
+
const assembly = assemblePi({
|
|
312
298
|
model: options.model,
|
|
313
299
|
thinkingLevel: options.thinkingLevel,
|
|
314
300
|
// THE directory rung's model surface: built-ins + the agent's own models.json (custom endpoints,
|
|
@@ -326,7 +312,7 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
326
312
|
// the finding set changes (boot findings are the baseline) — a runtime-written bad skill must
|
|
327
313
|
// not silently vanish from the agent, and a static one must not spam every turn's log. The
|
|
328
314
|
// next good edit heals both.
|
|
329
|
-
|
|
315
|
+
readDefinition: async () => {
|
|
330
316
|
const def = await loadAgentDefinition(dir, { cwd, env });
|
|
331
317
|
reportFindingsIfChanged(def.dir, def);
|
|
332
318
|
return {
|
|
@@ -336,10 +322,6 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
336
322
|
base: options.base ?? piBasePrompt({ tools, persona: def.persona }),
|
|
337
323
|
// ② project context: AGENTS.md files (agentDir + cwd-ancestor walk) via loadProjectContextFiles.
|
|
338
324
|
contextFiles: def.contextFiles,
|
|
339
|
-
skills: def.skills,
|
|
340
|
-
// The workspace, matching where the tools operate. Telling the model one directory while
|
|
341
|
-
// `read`/`bash` resolve relative paths against another is a lie it cannot detect.
|
|
342
|
-
cwd,
|
|
343
325
|
}),
|
|
344
326
|
skills: def.skills,
|
|
345
327
|
};
|
|
@@ -348,13 +330,16 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
348
330
|
sessions: options.sessions,
|
|
349
331
|
// Discovered so the serving assembly can WARN that it does not run them (and so the refusals
|
|
350
332
|
// apply to the artifact either way) — `chat` is where they load. Boot-resolved: the set cannot
|
|
351
|
-
// change without a restart, which is why this sits outside
|
|
333
|
+
// change without a restart, which is why this sits outside readDefinition above.
|
|
352
334
|
extensionPaths: await loadExtensionPaths(dir, { cwd, env }),
|
|
353
335
|
cwd,
|
|
354
336
|
env,
|
|
355
337
|
lease: options.lease,
|
|
356
|
-
observer: options.observer,
|
|
357
|
-
onAssembly: options.onAssembly,
|
|
358
338
|
});
|
|
359
|
-
return {
|
|
339
|
+
return { assembly, definition };
|
|
340
|
+
}
|
|
341
|
+
/** L2: "point at a directory → agent": {@link assemblePiFromDefinition} under the L0. */
|
|
342
|
+
export async function createPiAgentFromDefinition(dir, options) {
|
|
343
|
+
const { assembly, definition } = await assemblePiFromDefinition(dir, options);
|
|
344
|
+
return { agent: agentOf(assembly, options.observer), definition };
|
|
360
345
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ExecutionEnv, type Skill, type SkillDiagnostic } from "@earendil-works/pi-agent-core";
|
|
2
2
|
/** A same-name skill collision (the discarded side). Surfaced, never swallowed. */
|
|
3
3
|
export interface SkillCollision {
|
|
4
4
|
name: string;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { realpathSync } from "node:fs";
|
|
17
17
|
import { join, resolve } from "node:path";
|
|
18
|
-
import { loadSkills } from "@earendil-works/pi-agent-core";
|
|
18
|
+
import { BACKGROUND_CONTEXT, loadSkills, } from "@earendil-works/pi-agent-core";
|
|
19
19
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
20
20
|
import { loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { log } from "../../log.js";
|
|
@@ -26,7 +26,7 @@ export async function loadAgentDefinition(agentDir, options = {}) {
|
|
|
26
26
|
// never diverge if a caller passes a relative agentDir.
|
|
27
27
|
const cwd = options.cwd ?? agentDir;
|
|
28
28
|
const e = options.env ?? new NodeExecutionEnv({ cwd });
|
|
29
|
-
const rootResult = await e.absolutePath(agentDir);
|
|
29
|
+
const rootResult = await e.absolutePath(agentDir, BACKGROUND_CONTEXT);
|
|
30
30
|
if (!rootResult.ok) {
|
|
31
31
|
throw new Error(`cannot resolve agent dir "${agentDir}": ${rootResult.error.message}`);
|
|
32
32
|
}
|
|
@@ -38,7 +38,7 @@ export async function loadAgentDefinition(agentDir, options = {}) {
|
|
|
38
38
|
// persona.md → segment ① persona (overrides the identity line). Same error policy as AGENTS.md:
|
|
39
39
|
// only not_found means "absent"; any other read error surfaces rather than silently dropping the persona.
|
|
40
40
|
const personaPath = join(root, "persona.md");
|
|
41
|
-
const personaRead = await e.readTextFile(personaPath);
|
|
41
|
+
const personaRead = await e.readTextFile(personaPath, BACKGROUND_CONTEXT);
|
|
42
42
|
if (!personaRead.ok && personaRead.error.code !== "not_found") {
|
|
43
43
|
throw new Error(`cannot read ${personaPath}: ${personaRead.error.message}`);
|
|
44
44
|
}
|
|
@@ -86,13 +86,13 @@ export async function loadAgentDefinition(agentDir, options = {}) {
|
|
|
86
86
|
export async function loadExtensionPaths(agentDir, options = {}) {
|
|
87
87
|
const cwd = options.cwd ?? agentDir;
|
|
88
88
|
const e = options.env ?? new NodeExecutionEnv({ cwd });
|
|
89
|
-
const rootResult = await e.absolutePath(agentDir);
|
|
89
|
+
const rootResult = await e.absolutePath(agentDir, BACKGROUND_CONTEXT);
|
|
90
90
|
if (!rootResult.ok)
|
|
91
91
|
throw new Error(`cannot resolve agent dir "${agentDir}": ${rootResult.error.message}`);
|
|
92
92
|
const root = rootResult.value;
|
|
93
93
|
await assertInsideAgentDir(root, "extensions");
|
|
94
94
|
const dir = join(root, "extensions");
|
|
95
|
-
const listed = await e.listDir(dir);
|
|
95
|
+
const listed = await e.listDir(dir, BACKGROUND_CONTEXT);
|
|
96
96
|
if (!listed.ok) {
|
|
97
97
|
if (listed.error.code === "not_found")
|
|
98
98
|
return [];
|
|
@@ -131,15 +131,14 @@ export async function loadExtensionPaths(agentDir, options = {}) {
|
|
|
131
131
|
return paths.sort();
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
|
-
* The first candidate that is a REAL file
|
|
135
|
-
* subdirectory's `index.ts` could otherwise point
|
|
136
|
-
* top-level entries already follow.
|
|
134
|
+
* The first candidate that is a REAL file, and whether one was found but REFUSED as a symlink.
|
|
135
|
+
* `exists` would follow the link, which is how a subdirectory's `index.ts` could otherwise point
|
|
136
|
+
* outside the definition and slip past the rule the top-level entries already follow.
|
|
137
137
|
*/
|
|
138
|
-
/** The first real file among the candidates, and whether one was found but REFUSED as a symlink. */
|
|
139
138
|
async function firstRealFile(e, candidates) {
|
|
140
139
|
let refused = false;
|
|
141
140
|
for (const candidate of candidates) {
|
|
142
|
-
const info = await e.fileInfo(candidate);
|
|
141
|
+
const info = await e.fileInfo(candidate, BACKGROUND_CONTEXT);
|
|
143
142
|
if (!info.ok) {
|
|
144
143
|
if (info.error.code === "not_found")
|
|
145
144
|
continue;
|
|
@@ -165,7 +164,7 @@ async function readSkills(e, root) {
|
|
|
165
164
|
// definition loads the same skills on every machine — and, like tools/channels/schedules, a symlink
|
|
166
165
|
// that escapes the agent dir is refused rather than followed (the fourth of four surfaces).
|
|
167
166
|
await assertInsideAgentDir(root, "skills");
|
|
168
|
-
const { skills: raw, diagnostics } = await loadSkills(e, [join(root, "skills")]);
|
|
167
|
+
const { skills: raw, diagnostics } = await loadSkills(e, [join(root, "skills")], BACKGROUND_CONTEXT);
|
|
169
168
|
const byName = new Map();
|
|
170
169
|
const collisions = [];
|
|
171
170
|
for (const skill of raw) {
|
|
@@ -188,7 +187,7 @@ async function readSkills(e, root) {
|
|
|
188
187
|
export async function loadAgentSkills(agentDir, options = {}) {
|
|
189
188
|
const cwd = options.cwd ?? agentDir;
|
|
190
189
|
const e = options.env ?? new NodeExecutionEnv({ cwd });
|
|
191
|
-
const rootResult = await e.absolutePath(agentDir);
|
|
190
|
+
const rootResult = await e.absolutePath(agentDir, BACKGROUND_CONTEXT);
|
|
192
191
|
if (!rootResult.ok)
|
|
193
192
|
throw new Error(`cannot resolve agent dir "${agentDir}": ${rootResult.error.message}`);
|
|
194
193
|
// `dir` is the RESOLVED root, like {@link LoadedDefinition.dir}: readers key per-definition state
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ABORTED_CODE, SESSION_BUSY_CODE, } from "../../agent.js";
|
|
2
|
+
import { toRetryScheduledEvent } from "./retry-event.js";
|
|
2
3
|
import { cancellableStream } from "../../collect.js";
|
|
3
4
|
import { log } from "../../log.js";
|
|
4
5
|
import { EventQueue, errorToTerminal, inProcessLease, projectAgentEvent, toPiPromptOptions, toTerminal, } from "./turn-kit.js";
|
|
@@ -65,36 +66,9 @@ function toSessionEvent(event, runId) {
|
|
|
65
66
|
runId,
|
|
66
67
|
data: { steering: event.steering.length, followUp: event.followUp.length },
|
|
67
68
|
};
|
|
68
|
-
case "auto_retry_start":
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
timestamp: at,
|
|
72
|
-
runId,
|
|
73
|
-
data: {
|
|
74
|
-
operation: "assistant",
|
|
75
|
-
attempt: event.attempt,
|
|
76
|
-
maxAttempts: event.maxAttempts,
|
|
77
|
-
delayMs: event.delayMs,
|
|
78
|
-
error: event.errorMessage,
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
return retry;
|
|
82
|
-
}
|
|
83
|
-
case "summarization_retry_scheduled": {
|
|
84
|
-
const retry = {
|
|
85
|
-
type: "retry_scheduled",
|
|
86
|
-
timestamp: at,
|
|
87
|
-
runId,
|
|
88
|
-
data: {
|
|
89
|
-
operation: "compaction",
|
|
90
|
-
attempt: event.attempt,
|
|
91
|
-
maxAttempts: event.maxAttempts,
|
|
92
|
-
delayMs: event.delayMs,
|
|
93
|
-
error: event.errorMessage,
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
return retry;
|
|
97
|
-
}
|
|
69
|
+
case "auto_retry_start":
|
|
70
|
+
case "summarization_retry_scheduled":
|
|
71
|
+
return toRetryScheduledEvent(event, runId);
|
|
98
72
|
default:
|
|
99
73
|
return null;
|
|
100
74
|
}
|
|
@@ -240,6 +214,15 @@ export function createPiAgentFromSession(options) {
|
|
|
240
214
|
return; // decided; the retry's output is not ours
|
|
241
215
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
242
216
|
finalAssistant = event.message;
|
|
217
|
+
// Error messages and details can contain provider payloads; log only diagnostic metadata.
|
|
218
|
+
for (const diagnostic of finalAssistant.diagnostics ?? []) {
|
|
219
|
+
log.warn(`[fastagent] provider diagnostic ${diagnostic.type} (${finalAssistant.provider}/${finalAssistant.model}, session ${scope.session}, run ${runId})`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (event.type === "compaction_end" && event.reason !== "manual") {
|
|
223
|
+
const status = event.aborted ? "aborted" : event.errorMessage ? "failed" : "completed";
|
|
224
|
+
const emit = event.errorMessage && !event.aborted ? log.warn : log.debug;
|
|
225
|
+
emit(`[fastagent] automatic compaction ${event.reason} (session ${scope.session}, run ${runId}): ${status}`);
|
|
243
226
|
}
|
|
244
227
|
// pi retries a failed assistant request by DISCARDING that attempt's assistant message and
|
|
245
228
|
// asking again. Everything the turn achieved before it survives — executed tools keep
|
package/dist/engines/pi/login.js
CHANGED
|
@@ -30,6 +30,18 @@ function authCallbacks(io, userSignal, doneSignal) {
|
|
|
30
30
|
signal: userSignal ?? new AbortController().signal,
|
|
31
31
|
prompt: async (p) => {
|
|
32
32
|
if (p.type === "select") {
|
|
33
|
+
// This branch is UNCANCELLABLE: `LoginIO.select` takes no signal, so all three the line
|
|
34
|
+
// below composes — the provider's own, the caller's `loginFlow({ signal })`, and the
|
|
35
|
+
// pending-prompt backstop — are dropped here.
|
|
36
|
+
//
|
|
37
|
+
// It costs nothing against pi 0.84's providers, where every `select` is the FIRST call of
|
|
38
|
+
// `login()` (bedrock, vertex, openai-codex, radius — all of them asking which login method
|
|
39
|
+
// to use). Nothing is racing it: no callback server is up yet, and `doneSignal` fires when
|
|
40
|
+
// `auth.login()` RETURNS, which it cannot do while blocked on this await.
|
|
41
|
+
//
|
|
42
|
+
// That is a property of the providers, not a guarantee, and nothing here holds it. A
|
|
43
|
+
// provider that ever issues a `select` after starting its callback server shows up as the
|
|
44
|
+
// CLI parked on stdin — widen `LoginIO.select` with a signal then.
|
|
33
45
|
const v = await io.select(p.message, p.options.map((o) => ({ value: o.id, label: o.label, hint: o.description })));
|
|
34
46
|
if (v === undefined)
|
|
35
47
|
throw new LoginCancelled("cancelled");
|
|
@@ -94,9 +106,14 @@ async function selectProvider(io, providers, method, store) {
|
|
|
94
106
|
return { value: p.id, label: auth?.name ?? p.name, hint: cred ? `configured (${cred.type})` : undefined };
|
|
95
107
|
}));
|
|
96
108
|
const id = await io.select("Select a provider", options);
|
|
97
|
-
|
|
109
|
+
// Answers the PROVIDER, not its id: the caller needs the object, and resolving it here means the
|
|
110
|
+
// one place that can fail to is the one that just offered the list. Cancel and an id that was
|
|
111
|
+
// never offered are the same answer — nothing was chosen — and `find` gives both, since a
|
|
112
|
+
// provider id is always a string and `undefined` matches none of them.
|
|
113
|
+
const chosen = candidates.find((p) => p.id === id);
|
|
114
|
+
if (!chosen)
|
|
98
115
|
throw new LoginCancelled("no provider selected");
|
|
99
|
-
return
|
|
116
|
+
return chosen;
|
|
100
117
|
}
|
|
101
118
|
/**
|
|
102
119
|
* Resolve method + provider (asking only what is not given), run the login flow, and persist. A no-op
|
|
@@ -106,26 +123,25 @@ export async function loginFlow(io, options = {}) {
|
|
|
106
123
|
const store = options.store ?? fastagentCredentialStore(options.authPath ?? GLOBAL_AUTH_PATH);
|
|
107
124
|
const providers = options.providers ?? builtinProviders();
|
|
108
125
|
let method;
|
|
109
|
-
let
|
|
126
|
+
let provider;
|
|
110
127
|
if (options.provider) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
method = options.method ?? (await methodForProvider(io,
|
|
128
|
+
const named = providers.find((x) => x.id === options.provider);
|
|
129
|
+
if (!named)
|
|
130
|
+
throw new Error(`unknown provider "${options.provider}"`);
|
|
131
|
+
provider = named;
|
|
132
|
+
method = options.method ?? (await methodForProvider(io, provider));
|
|
116
133
|
}
|
|
117
134
|
else {
|
|
118
135
|
method = options.method ?? (await selectMethod(io));
|
|
119
|
-
|
|
136
|
+
provider = await selectProvider(io, providers, method, store);
|
|
120
137
|
}
|
|
121
138
|
// Preflight: a no-op modify runs the refuse-corrupt / writability check BEFORE the flow.
|
|
122
|
-
await store.modify(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
throw new Error(`unknown provider "${providerId}"`);
|
|
139
|
+
await store.modify(provider.id, async () => undefined);
|
|
140
|
+
// Reachable even though both branches above resolved a provider: an explicit `method` bypasses
|
|
141
|
+
// methodForProvider, so `{ provider: "openai-codex", method: "api_key" }` arrives here.
|
|
126
142
|
const auth = method === "oauth" ? provider.auth.oauth : provider.auth.apiKey;
|
|
127
143
|
if (!auth?.login)
|
|
128
|
-
throw new Error(`provider "${
|
|
144
|
+
throw new Error(`provider "${provider.id}" has no ${method} login`);
|
|
129
145
|
// `done` cancels any prompt left pending when login resolves (manual-code race backstop).
|
|
130
146
|
const done = new AbortController();
|
|
131
147
|
let credential;
|
|
@@ -135,6 +151,6 @@ export async function loginFlow(io, options = {}) {
|
|
|
135
151
|
finally {
|
|
136
152
|
done.abort();
|
|
137
153
|
}
|
|
138
|
-
await store.modify(
|
|
139
|
-
return { provider:
|
|
154
|
+
await store.modify(provider.id, async () => credential);
|
|
155
|
+
return { provider: provider.id, method };
|
|
140
156
|
}
|
|
@@ -5,8 +5,7 @@ import type { SessionObserver } from "./turn-kit.ts";
|
|
|
5
5
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
6
6
|
import { type LoadedDefinition } from "./definition.ts";
|
|
7
7
|
import { type PiSessionRecordStore } from "./session-store.ts";
|
|
8
|
-
import type { ToolCollision } from "./tool.ts";
|
|
9
|
-
import type { MountedTool } from "./tool.ts";
|
|
8
|
+
import type { ToolCollision, MountedTool } from "./tool.ts";
|
|
10
9
|
export interface CreatePiAgentFromDirOptions {
|
|
11
10
|
/** Model spec override (e.g. the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
|
|
12
11
|
model?: string;
|
|
@@ -37,8 +36,8 @@ export interface CreatePiAgentFromDirOptions {
|
|
|
37
36
|
sessionControl?: boolean;
|
|
38
37
|
/** Additional raw tap with the FULL vocabulary: run events composed after the
|
|
39
38
|
* {@link sessionControl} hub's observer, plus the hub's own boundary-mutation events
|
|
40
|
-
* (`state_changed`/`compaction_*`) via the hub's tap. TRUSTED seam:
|
|
41
|
-
*
|
|
39
|
+
* (`state_changed`/`compaction_*`) via the hub's tap. TRUSTED seam: an observer receives each
|
|
40
|
+
* run's live modulation handles (see `SessionObserver`) — for read-only consumers
|
|
42
41
|
* use the hub's `events()` stream instead. */
|
|
43
42
|
observer?: SessionObserver;
|
|
44
43
|
}
|
package/dist/engines/pi/open.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { mkdir } from "node:fs/promises";
|
|
12
12
|
import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModelSpec, } from "./config.js";
|
|
13
13
|
import { resolveStateRoot, resolvePlacement } from "../../paths.js";
|
|
14
|
-
import {
|
|
14
|
+
import { agentOf, assemblePiFromDefinition, resolveAgentTools } from "./create.js";
|
|
15
15
|
import { createPiSessionControl } from "./session-control.js";
|
|
16
16
|
import { withWakeTool } from "./wake-tool.js";
|
|
17
17
|
import { loadAgentSkills } from "./definition.js";
|
|
@@ -63,19 +63,36 @@ export async function createPiAgentFromDir(dir, options = {}) {
|
|
|
63
63
|
const sessionsDir = options.sessionsDir ?? defaultSessionsDir(stateRoot);
|
|
64
64
|
await mkdir(sessionsDir, { recursive: true });
|
|
65
65
|
const sessions = piSessionRecordStore({ dir: sessionsDir, cwd: workspace });
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
const { assembly, definition } = await assemblePiFromDefinition(agentDir, {
|
|
67
|
+
model: modelSpec,
|
|
68
|
+
thinkingLevel: config.thinkingLevel,
|
|
69
|
+
cwd: workspace,
|
|
70
|
+
tools: mountedTools,
|
|
71
|
+
authPath,
|
|
72
|
+
// Skills are definition-only (the agent is its directory), so dev mirrors deployment exactly.
|
|
73
|
+
sessions,
|
|
74
|
+
});
|
|
75
|
+
// The hub is wired HERE because the store is created here (an external `createPiSessionControl`
|
|
76
|
+
// cannot exist before the store does). Its writes contend on the assembly's own lease and validate
|
|
77
|
+
// against its own registry — the value above is what makes "same" true. An extra caller observer
|
|
78
|
+
// composes after the hub's (TRUSTED seam).
|
|
73
79
|
const caller = options.observer;
|
|
74
80
|
const wantControl = options.sessionControl ?? (config.sessionControl === true && options.serving === true);
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
let hub;
|
|
82
|
+
if (wantControl) {
|
|
83
|
+
// The hub's surface is synchronous (`capabilities()` lists the allowed models), while building
|
|
84
|
+
// the registry reads credentials and is not. Resolve it ONCE here — the opener is async anyway,
|
|
85
|
+
// and the first turn would have paid the same read — so every boundary mutation validates
|
|
86
|
+
// against the same registry the runs use.
|
|
87
|
+
const { modelRuntime, model } = await assembly.engine();
|
|
88
|
+
hub = createPiSessionControl({
|
|
77
89
|
sessions,
|
|
78
|
-
boundary:
|
|
90
|
+
boundary: {
|
|
91
|
+
lease: assembly.lease,
|
|
92
|
+
models: modelRuntime,
|
|
93
|
+
sessionFactory: assembly.sessionFactory,
|
|
94
|
+
defaults: { model, thinkingLevel: assembly.thinkingLevel },
|
|
95
|
+
},
|
|
79
96
|
// Skills ARE the names a client offers — the resolved set, after collisions were decided
|
|
80
97
|
// first-wins, which a client cannot reconstruct from the directory. Read LIVE (the directory
|
|
81
98
|
// is the agent: a skill added while serving is in play on the next turn, so it must be
|
|
@@ -95,10 +112,10 @@ export async function createPiAgentFromDir(dir, options = {}) {
|
|
|
95
112
|
},
|
|
96
113
|
// The caller tap's boundary-event half: state_changed/compaction_* originate in the hub
|
|
97
114
|
// and never cross the data plane's observer seam — without this, an audit tap wired here
|
|
98
|
-
// would miss exactly the mutations it most needs to see (
|
|
115
|
+
// would miss exactly the mutations it most needs to see (`update({ model })`).
|
|
99
116
|
tap: caller ? (session, event) => caller(session, event) : undefined,
|
|
100
|
-
})
|
|
101
|
-
|
|
117
|
+
});
|
|
118
|
+
}
|
|
102
119
|
const observer = hub
|
|
103
120
|
? caller
|
|
104
121
|
? (session, event, run) => {
|
|
@@ -107,34 +124,7 @@ export async function createPiAgentFromDir(dir, options = {}) {
|
|
|
107
124
|
}
|
|
108
125
|
: hub.observer
|
|
109
126
|
: caller;
|
|
110
|
-
const
|
|
111
|
-
model: modelSpec,
|
|
112
|
-
thinkingLevel: config.thinkingLevel,
|
|
113
|
-
cwd: workspace,
|
|
114
|
-
tools: mountedTools,
|
|
115
|
-
authPath,
|
|
116
|
-
// Skills are definition-only (the agent is its directory), so dev mirrors deployment exactly.
|
|
117
|
-
sessions,
|
|
118
|
-
observer,
|
|
119
|
-
onAssembly: hub
|
|
120
|
-
? (parts) => {
|
|
121
|
-
assembled = parts;
|
|
122
|
-
}
|
|
123
|
-
: undefined,
|
|
124
|
-
});
|
|
125
|
-
if (hub && assembled) {
|
|
126
|
-
// The hub's surface is synchronous (`capabilities()` lists the allowed models), while building
|
|
127
|
-
// the registry reads credentials and is not. Resolve it ONCE here — the opener is async anyway,
|
|
128
|
-
// and the first turn would have paid the same read — so every boundary mutation validates
|
|
129
|
-
// against the same registry the runs use.
|
|
130
|
-
const { modelRuntime, model } = await assembled.engine();
|
|
131
|
-
boundaryParts = {
|
|
132
|
-
lease: assembled.lease,
|
|
133
|
-
models: modelRuntime,
|
|
134
|
-
sessionFactory: assembled.sessionFactory,
|
|
135
|
-
defaults: { model, thinkingLevel: assembled.thinkingLevel },
|
|
136
|
-
};
|
|
137
|
-
}
|
|
127
|
+
const agent = agentOf(assembly, observer);
|
|
138
128
|
return {
|
|
139
129
|
agent,
|
|
140
130
|
definition,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { RetryScheduledEvent } from "../../session.ts";
|
|
3
|
+
/** Manual compaction retries are session-scoped; retries inside an invoke carry its runId. */
|
|
4
|
+
export declare function toRetryScheduledEvent(event: Extract<AgentSessionEvent, {
|
|
5
|
+
type: "auto_retry_start" | "summarization_retry_scheduled";
|
|
6
|
+
}>, runId?: string): RetryScheduledEvent;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Manual compaction retries are session-scoped; retries inside an invoke carry its runId. */
|
|
2
|
+
export function toRetryScheduledEvent(event, runId) {
|
|
3
|
+
return {
|
|
4
|
+
type: "retry_scheduled",
|
|
5
|
+
timestamp: Date.now(),
|
|
6
|
+
...(runId === undefined ? {} : { runId }),
|
|
7
|
+
data: {
|
|
8
|
+
operation: event.type === "auto_retry_start" ? "assistant" : "compaction",
|
|
9
|
+
attempt: event.attempt,
|
|
10
|
+
maxAttempts: event.maxAttempts,
|
|
11
|
+
delayMs: event.delayMs,
|
|
12
|
+
error: event.errorMessage,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|