@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
|
@@ -4,73 +4,97 @@
|
|
|
4
4
|
*
|
|
5
5
|
* L2 createPiAgentFromDefinition(dir, options) — load a definition directory, assemble, then L1.
|
|
6
6
|
* L1 createPiAgent(options) — assemble from typed parts (the canonical ctor).
|
|
7
|
-
* L0
|
|
7
|
+
* L0 createPiAgentFromSession({ sessionFactory }) — in invoke-session.ts (the turn mechanism).
|
|
8
8
|
*
|
|
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
12
|
*/
|
|
13
13
|
import { formatSkillsForSystemPrompt } from "@earendil-works/pi-agent-core";
|
|
14
|
-
import { createBashTool, createEditTool, createReadTool, createWriteTool, } from "@earendil-works/pi-agent-core";
|
|
15
14
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
16
|
-
import {
|
|
15
|
+
import { createCodingTools, createPowerShellTool, createReadOnlyTools } from "@earendil-works/pi-coding-agent";
|
|
17
16
|
import { defaultAuthPath, resolveModel } from "./config.js";
|
|
18
17
|
import { resolveSecretsDir } from "../../paths.js";
|
|
19
|
-
import { loadAgentDefinition } from "./definition.js";
|
|
20
|
-
import { DEFAULT_THINKING_LEVEL, piHarnessFactory } from "./harness.js";
|
|
21
|
-
import { createPiModelRuntime, createPiModels } from "./models.js";
|
|
18
|
+
import { loadAgentDefinition, loadExtensionPaths } from "./definition.js";
|
|
22
19
|
import { reportFindingsIfChanged } from "./report.js";
|
|
23
|
-
import { inMemorySessionStore } from "./sessions.js";
|
|
24
20
|
import { isDeferredTool, loadTools, mergeDiscoveredTools, } from "./tool.js";
|
|
25
21
|
import { withSearchTool } from "./search-tools.js";
|
|
26
|
-
import {
|
|
22
|
+
import { createPiAgentFromSession } from "./invoke-session.js";
|
|
23
|
+
import { piAgentSessionFactory } from "./agent-session-factory.js";
|
|
24
|
+
import { DEFAULT_THINKING_LEVEL, createPiModelRuntime } from "./models.js";
|
|
25
|
+
import { piInMemorySessionRecordStore } from "./session-store.js";
|
|
27
26
|
import { inProcessLease } from "./turn-kit.js";
|
|
28
27
|
// ── §1 tools ─────────────────────────────────────────────────────────────────
|
|
29
28
|
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
29
|
+
// A directory agent gets every pi coding tool. Restricting what those tools can reach belongs at the
|
|
30
|
+
// sandbox boundary, not in an allowlist that leaves authored tools unrestricted. L1/L2 library callers
|
|
31
|
+
// can still replace the coding defaults through `tools`; deferred tools may add `search_tools`.
|
|
33
32
|
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// that governed everything except the tools that actually touch the machine. (It buys no decoupling
|
|
39
|
-
// from pi-coding-agent — definition.ts, models.ts and read-image.ts all import it regardless.)
|
|
33
|
+
// This is not a security boundary and no default here could be one: the built-in POST /invoke has no
|
|
34
|
+
// authentication, author-written `tools/` import whatever they like, and a WebSocket or Socket-Mode
|
|
35
|
+
// channel dials OUT, so no bind address constrains who can message the agent. Whoever can reach an
|
|
36
|
+
// agent can use everything it mounts — put it behind something that decides who may.
|
|
40
37
|
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
38
|
+
// All seven come from pi-coding-agent and reach the machine through `node:fs`, bypassing the per-turn
|
|
39
|
+
// {@link ExecutionEnv}. pi-agent-core ships env-routed look-alikes, but that seam narrows a blast
|
|
40
|
+
// radius rather than closing one (authored `tools/` ignore it), so a real sandbox must wrap the
|
|
41
|
+
// process instead.
|
|
44
42
|
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
// Chat receives the same tool objects through pi's `customTools` path; pi's builtin copies stay off.
|
|
44
|
+
/**
|
|
45
|
+
* Every pi coding tool, in canonical order, rooted at the workspace it operates in.
|
|
46
|
+
*
|
|
47
|
+
* pi ships two overlapping groupings and neither is the whole set: `createCodingTools` is the four it
|
|
48
|
+
* ACTIVATES for a terminal (read/bash/edit/write, no searching), `createReadOnlyTools` is
|
|
49
|
+
* read/grep/find/ls. `read` is in both; a directory agent mounts their union.
|
|
50
|
+
*/
|
|
51
|
+
export const CODING_TOOL_NAMES = ["read", "grep", "find", "ls", "bash", "edit", "write"];
|
|
52
|
+
export function piAllCodingTools(cwd) {
|
|
53
|
+
const mutating = createCodingTools(cwd).filter((tool) => tool.name !== "read");
|
|
54
|
+
return [...createReadOnlyTools(cwd), ...mutating];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Every tool an `AgentSession` registers on its own, whether or not a directory agent mounts it.
|
|
58
|
+
*
|
|
59
|
+
* Wider than {@link CODING_TOOL_NAMES} on purpose: pi 0.84.3 added `powershell`, which the session
|
|
60
|
+
* registers regardless of the tools we pass. A name absent from THIS list is a name never excluded,
|
|
61
|
+
* and an excluded name is the only thing that cannot be activated — so leaving it out would let
|
|
62
|
+
* `tools: [read]` keep a reachable shell.
|
|
63
|
+
*/
|
|
64
|
+
function piRegisteredToolNames(cwd) {
|
|
51
65
|
return [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
createWriteTool(),
|
|
66
|
+
...createReadOnlyTools(cwd).map((tool) => tool.name),
|
|
67
|
+
...createCodingTools(cwd).map((tool) => tool.name),
|
|
68
|
+
createPowerShellTool(cwd).name,
|
|
56
69
|
];
|
|
57
70
|
}
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
return
|
|
71
|
+
/** Built-ins omitted by an explicit lower-level tool list. A reused name stays mounted. */
|
|
72
|
+
function omittedBuiltinNames(mounted, cwd) {
|
|
73
|
+
const mountedNames = new Set(mounted.map((tool) => tool.name));
|
|
74
|
+
return [...new Set(piRegisteredToolNames(cwd))].filter((name) => !mountedNames.has(name));
|
|
62
75
|
}
|
|
63
76
|
/**
|
|
64
|
-
* The full tool set
|
|
65
|
-
* existing win), plus the
|
|
77
|
+
* The full directory-agent tool set: all pi coding tools + `config.tools` + discovered `tools/`
|
|
78
|
+
* (deduped, existing win), plus the authored names and collisions to report. One source for the
|
|
66
79
|
* dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
|
|
67
80
|
*/
|
|
68
|
-
export async function resolveAgentTools(config, agentDir) {
|
|
69
|
-
// Discovered `tools/` come from `agentDir` (the agent's own surface); the
|
|
70
|
-
//
|
|
71
|
-
// the
|
|
81
|
+
export async function resolveAgentTools(config, agentDir, cwd) {
|
|
82
|
+
// Discovered `tools/` come from `agentDir` (the agent's own surface); the coding tools are
|
|
83
|
+
// rooted at `cwd`, the WORKSPACE — the project the agent works on, which is where an author expects
|
|
84
|
+
// `read`/`bash` to land, and which is not always the definition directory.
|
|
72
85
|
const discovered = await loadTools(agentDir);
|
|
73
|
-
const
|
|
86
|
+
const configured = piAllCodingTools(cwd);
|
|
87
|
+
const configuredNames = new Set(configured.map((tool) => tool.name));
|
|
88
|
+
const configuredCollisions = [];
|
|
89
|
+
for (const tool of config.tools ?? []) {
|
|
90
|
+
if (configuredNames.has(tool.name)) {
|
|
91
|
+
configuredCollisions.push({ name: tool.name, source: "config.tools" });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
configuredNames.add(tool.name);
|
|
95
|
+
configured.push(tool);
|
|
96
|
+
}
|
|
97
|
+
const merged = mergeDiscoveredTools(configured, discovered.tools);
|
|
74
98
|
// The built-in `search_tools` loader mounts here — the one place the agent's full tool set is
|
|
75
99
|
// computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; an
|
|
76
100
|
// agent-defined search_tools wins).
|
|
@@ -78,12 +102,12 @@ export async function resolveAgentTools(config, agentDir) {
|
|
|
78
102
|
// Builtin = a search_tools that was ABSENT before withSearchTool (a reference compare would misfire
|
|
79
103
|
// on the deferred-authored-loader case, where withSearchTool returns a new array without adding one).
|
|
80
104
|
const builtinLoaderMounted = !merged.tools.some((t) => t.name === "search_tools") && tools.some((t) => t.name === "search_tools");
|
|
81
|
-
const toolCollisions = [...discovered.collisions, ...merged.collisions];
|
|
82
|
-
// `toolNames` is the AUTHOR's active-by-default surface (config.tools + tools/): exclude pi
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
const defaultNames = new Set(
|
|
105
|
+
const toolCollisions = [...discovered.collisions, ...configuredCollisions, ...merged.collisions];
|
|
106
|
+
// `toolNames` is the AUTHOR's active-by-default surface (config.tools + tools/): exclude pi coding
|
|
107
|
+
// tools, the builtin loader (like wake, a builtin gets its own report line, not an anonymous slot in
|
|
108
|
+
// the author's list — an author-DEFINED search_tools still shows), and deferred tools. Each name
|
|
109
|
+
// lives in exactly ONE report slot, and deferred names live in `deferredToolNames`.
|
|
110
|
+
const defaultNames = new Set(CODING_TOOL_NAMES);
|
|
87
111
|
const toolNames = tools
|
|
88
112
|
.filter((t) => !defaultNames.has(t.name) && !isDeferredTool(t) && !(builtinLoaderMounted && t.name === "search_tools"))
|
|
89
113
|
.map((t) => t.name);
|
|
@@ -123,9 +147,16 @@ export function piBasePrompt(options = {}) {
|
|
|
123
147
|
const deferredCount = mounted.length - tools.length;
|
|
124
148
|
const toolsList = tools.length > 0 ? tools.map((t) => `- ${t.name}: ${(t.description ?? "").split("\n")[0]}`).join("\n") : "(none)";
|
|
125
149
|
// Segment ① identity: an authored persona (persona.md) replaces the default engine identity line
|
|
126
|
-
// (core.md §11), keeping the tools list + guidelines below.
|
|
150
|
+
// (core.md §11), keeping the tools list + guidelines below. Preserve pi's coding identity only for
|
|
151
|
+
// the full coding surface; a partial/empty surface must not claim machine capabilities it lacks.
|
|
152
|
+
// The four this sentence NAMES — reading, executing, editing, writing. Searching is not part of the
|
|
153
|
+
// claim, so requiring it would demote an agent that can do everything the identity says it can.
|
|
154
|
+
const mountedNames = new Set(mounted.map((tool) => tool.name));
|
|
155
|
+
const fullCodingSurface = ["read", "bash", "edit", "write"].every((name) => mountedNames.has(name));
|
|
127
156
|
const identity = options.persona?.trim() ||
|
|
128
|
-
|
|
157
|
+
(fullCodingSurface
|
|
158
|
+
? "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."
|
|
159
|
+
: "You are an AI assistant operating inside pi, an agent harness. Help users using only the tools and context available to you.");
|
|
129
160
|
const deferredNote = deferredCount > 0
|
|
130
161
|
? `\n\n${deferredCount} additional tool(s) are registered but inactive — use search_tools to discover and activate them before concluding a capability is missing.`
|
|
131
162
|
: "";
|
|
@@ -165,30 +196,56 @@ export function assembleSystemPrompt(options) {
|
|
|
165
196
|
* directory) and route through here.
|
|
166
197
|
*/
|
|
167
198
|
function buildPiAgent(opts) {
|
|
168
|
-
const models = opts.models ?? createPiModels({ providers: opts.providers, authPath: opts.authPath });
|
|
169
199
|
const env = opts.env ?? new NodeExecutionEnv({ cwd: process.cwd() });
|
|
170
|
-
|
|
171
|
-
//
|
|
200
|
+
const cwd = opts.cwd ?? env.cwd;
|
|
201
|
+
// Materialized here (not defaulted inside the L0) so the exposed parts carry the SAME lease
|
|
202
|
+
// instance the agent runs under — boundary mutations must contend on it.
|
|
172
203
|
const lease = opts.lease ?? inProcessLease();
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
204
|
+
const sessions = opts.sessions ?? piInMemorySessionRecordStore({ cwd });
|
|
205
|
+
// The model and its runtime resolve on FIRST USE: building a ModelRuntime is async while
|
|
206
|
+
// assembling an agent is not, so the credential read belongs on the first turn rather than in the
|
|
207
|
+
// caller's constructor. Memoized by the factory, and shared with the control plane below so a
|
|
208
|
+
// boundary mutation validates against the registry the runs actually use.
|
|
209
|
+
let engine;
|
|
210
|
+
const resolveEngine = () => {
|
|
211
|
+
engine ??= (async () => {
|
|
212
|
+
// The caller's registry when there is one — the directory rung builds it from the agent's own
|
|
213
|
+
// models.json, so a custom endpoint declared there is the one a turn resolves against.
|
|
214
|
+
const modelRuntime = opts.models ?? (await createPiModelRuntime({ authPath: opts.authPath }));
|
|
215
|
+
// ModelRuntime registers providers by config record, so an injected Provider INSTANCE (a
|
|
216
|
+
// gateway, a self-hosted endpoint, a test fake) goes in through its native seam.
|
|
217
|
+
for (const provider of opts.providers ?? [])
|
|
218
|
+
modelRuntime.registerNativeProvider(provider);
|
|
219
|
+
return { modelRuntime, model: resolveModel(modelRuntime, opts.model) };
|
|
220
|
+
})();
|
|
221
|
+
return engine;
|
|
178
222
|
};
|
|
179
|
-
const
|
|
180
|
-
sessions
|
|
181
|
-
|
|
182
|
-
models,
|
|
183
|
-
model: defaults.model,
|
|
223
|
+
const sessionFactory = piAgentSessionFactory({
|
|
224
|
+
sessions,
|
|
225
|
+
engine: resolveEngine,
|
|
184
226
|
thinkingLevel: opts.thinkingLevel,
|
|
185
|
-
systemPrompt: opts.systemPrompt,
|
|
186
227
|
tools: opts.tools,
|
|
228
|
+
systemPrompt: opts.systemPrompt,
|
|
187
229
|
skills: opts.skills,
|
|
188
230
|
live: opts.live,
|
|
231
|
+
cwd,
|
|
232
|
+
...(opts.agentDir ? { agentDir: opts.agentDir } : {}),
|
|
233
|
+
...(opts.extensionPaths ? { extensionPaths: opts.extensionPaths } : {}),
|
|
234
|
+
// `noTools: "builtin"` leaves pi's built-ins in the registry; a lower-level replacement must also
|
|
235
|
+
// deny every omitted coding name so a loader cannot reactivate one later.
|
|
236
|
+
excludedToolNames: omittedBuiltinNames(opts.tools ?? [], cwd),
|
|
237
|
+
env,
|
|
238
|
+
});
|
|
239
|
+
opts.onAssembly?.({
|
|
240
|
+
lease,
|
|
241
|
+
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
|
+
engine: resolveEngine,
|
|
246
|
+
thinkingLevel: opts.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
189
247
|
});
|
|
190
|
-
|
|
191
|
-
return createPiAgentFromHarness({ lease, observer: opts.observer, cwd: env.cwd, harnessFactory });
|
|
248
|
+
return createPiAgentFromSession({ lease, observer: opts.observer, sessionFactory });
|
|
192
249
|
}
|
|
193
250
|
/**
|
|
194
251
|
* L1 system prompt: `instructions` ARE the prompt (no engine base, no wrapping); the skills listing
|
|
@@ -233,15 +290,21 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
233
290
|
const env = options.env ?? new NodeExecutionEnv({ cwd });
|
|
234
291
|
// Boot-time load: fail-visibly at startup on a broken directory, and give callers the snapshot to
|
|
235
292
|
// report (skills/diagnostics/collisions). Serving does NOT close over it — see `live` below.
|
|
236
|
-
|
|
293
|
+
// `cwd`, not `env.cwd`, for the same reason as the tools below: `cwd` is the run root whose
|
|
294
|
+
// ancestors carry ② project context. Reading it off the env pointed the AGENTS.md walk at the
|
|
295
|
+
// loader's directory whenever a caller supplied both.
|
|
296
|
+
const definition = await loadAgentDefinition(dir, { cwd, env });
|
|
297
|
+
// Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
|
|
298
|
+
// it; a caller's own search_tools wins).
|
|
299
|
+
// `cwd`, not `env.cwd`: the workspace is what this option MEANS, and a caller may hand a custom env
|
|
300
|
+
// for definition loading whose root is a different directory. Taking it off the env would silently
|
|
301
|
+
// point the coding tools at the loader's directory.
|
|
302
|
+
const tools = withSearchTool(options.tools ?? piAllCodingTools(cwd));
|
|
237
303
|
// Boot findings go through the SAME memoized reporter every later reader uses (report.ts, keyed by
|
|
238
304
|
// the resolved dir): announced once here, and re-announced by a turn or by the control plane's
|
|
239
305
|
// command list only when the set CHANGES — a runtime-written bad skill surfaces the moment it
|
|
240
306
|
// appears, a static one does not spam. Log dedup, not session state (stateless invoke holds).
|
|
241
307
|
reportFindingsIfChanged(definition.dir, definition);
|
|
242
|
-
// Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
|
|
243
|
-
// it; a caller's own search_tools wins).
|
|
244
|
-
const tools = withSearchTool(options.tools ?? piDefaultTools());
|
|
245
308
|
// Dir-aware default: the same secrets-dir-derived file the opener uses for this dir (the opener
|
|
246
309
|
// passes an explicit authPath, so this only affects direct L2 callers).
|
|
247
310
|
const authPath = options.authPath ?? defaultAuthPath(resolveSecretsDir(dir));
|
|
@@ -264,7 +327,7 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
264
327
|
// not silently vanish from the agent, and a static one must not spam every turn's log. The
|
|
265
328
|
// next good edit heals both.
|
|
266
329
|
live: async () => {
|
|
267
|
-
const def = await loadAgentDefinition(dir, { cwd
|
|
330
|
+
const def = await loadAgentDefinition(dir, { cwd, env });
|
|
268
331
|
reportFindingsIfChanged(def.dir, def);
|
|
269
332
|
return {
|
|
270
333
|
systemPrompt: assembleSystemPrompt({
|
|
@@ -274,13 +337,20 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
274
337
|
// ② project context: AGENTS.md files (agentDir + cwd-ancestor walk) via loadProjectContextFiles.
|
|
275
338
|
contextFiles: def.contextFiles,
|
|
276
339
|
skills: def.skills,
|
|
277
|
-
|
|
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,
|
|
278
343
|
}),
|
|
279
344
|
skills: def.skills,
|
|
280
345
|
};
|
|
281
346
|
},
|
|
282
347
|
tools,
|
|
283
348
|
sessions: options.sessions,
|
|
349
|
+
// Discovered so the serving assembly can WARN that it does not run them (and so the refusals
|
|
350
|
+
// 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 `live` above.
|
|
352
|
+
extensionPaths: await loadExtensionPaths(dir, { cwd, env }),
|
|
353
|
+
cwd,
|
|
284
354
|
env,
|
|
285
355
|
lease: options.lease,
|
|
286
356
|
observer: options.observer,
|
|
@@ -41,6 +41,47 @@ export interface LoadAgentDefinitionOptions {
|
|
|
41
41
|
}
|
|
42
42
|
/** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
|
|
43
43
|
export declare function loadAgentDefinition(agentDir: string, options?: LoadAgentDefinitionOptions): Promise<LoadedDefinition>;
|
|
44
|
+
/**
|
|
45
|
+
* Extension entry-point FILES under `<agentDir>/extensions/`, empty when there are none.
|
|
46
|
+
*
|
|
47
|
+
* Paths, not loaded objects — unlike skills (data: content inline, serializable), an extension is
|
|
48
|
+
* CODE that pi loads with jiti and binds to its own eventBus/runtime, so loading it here would
|
|
49
|
+
* reimplement pi's loader. The engine binding hands these to pi as `additionalExtensionPaths`, which
|
|
50
|
+
* survives `noExtensions: true` — that flag suppresses MACHINE-GLOBAL discovery (`~/.pi`), and the
|
|
51
|
+
* definition's own extensions were only ever collateral to it. FILES, not the directory: pi's paths
|
|
52
|
+
* are module specifiers, and a directory fails as `Cannot find module` into a
|
|
53
|
+
* `LoadExtensionsResult.errors` entry nothing reads.
|
|
54
|
+
*
|
|
55
|
+
* SEPARATE from {@link loadAgentDefinition} on purpose. Prompt and skills are re-read every invoke
|
|
56
|
+
* ("the directory is the agent, LIVE"); scanning for extension entry points is not, since the set
|
|
57
|
+
* cannot change without a restart. Called once per assembly, like `tools/`.
|
|
58
|
+
*
|
|
59
|
+
* Discovery runs on BOTH paths; loading does not. `chat` hands these to pi and runs them fully.
|
|
60
|
+
* Serving only announces them — pi's extension runtime is shared across sessions, and serving has
|
|
61
|
+
* concurrent turns for unrelated conversations (see `PiAgentSessionFactoryOptions.extensionPaths`).
|
|
62
|
+
* The refusals below are therefore about what the ARTIFACT may contain, and hold for both.
|
|
63
|
+
*
|
|
64
|
+
* Discovery follows pi's own rules, so an extension that works in pi works here:
|
|
65
|
+
*
|
|
66
|
+
* 1. a direct `*.ts` / `*.js` file;
|
|
67
|
+
* 2. a subdirectory with `index.ts` / `index.js`.
|
|
68
|
+
*
|
|
69
|
+
* pi has a third rule — a subdirectory whose `package.json` declares a `pi` field — which is NOT
|
|
70
|
+
* implemented here. That shape is reported rather than skipped: a definition whose extension silently
|
|
71
|
+
* fails to load is the failure mode this whole path exists to remove.
|
|
72
|
+
*
|
|
73
|
+
* Containment matches skills/tools/channels/schedules (the fifth of five surfaces): a symlinked
|
|
74
|
+
* `extensions/` escaping the agent dir is refused, and — like `loadModuleDir`, whose `entry.isFile()`
|
|
75
|
+
* excludes them — a symlinked ENTRY is not loaded either. pi's own discovery does follow those, but
|
|
76
|
+
* an extension reached through a link out of the definition is code the artifact does not carry: it
|
|
77
|
+
* resolves on the authoring machine and is missing in the container. Refused loudly, never silently.
|
|
78
|
+
*
|
|
79
|
+
* Absent is normal and silent; a FILE at that path is not — that is an author who meant something.
|
|
80
|
+
*/
|
|
81
|
+
export declare function loadExtensionPaths(agentDir: string, options?: {
|
|
82
|
+
cwd?: string;
|
|
83
|
+
env?: ExecutionEnv;
|
|
84
|
+
}): Promise<string[]>;
|
|
44
85
|
/**
|
|
45
86
|
* The definition's skills ALONE, resolved the same way `loadAgentDefinition` resolves them (same
|
|
46
87
|
* loader, same containment guard, same first-wins collision rule) — for readers that need only the
|
|
@@ -56,13 +97,6 @@ export declare function loadAgentSkills(agentDir: string, options?: {
|
|
|
56
97
|
collisions: SkillCollision[];
|
|
57
98
|
dir: string;
|
|
58
99
|
}>;
|
|
59
|
-
/**
|
|
60
|
-
* Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
|
|
61
|
-
* this OUT of the agent?" — the startup report's redeploy notes, `add`'s printed `.env` label, and the
|
|
62
|
-
* dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
|
|
63
|
-
* does not act on where a user's paths point.
|
|
64
|
-
*/
|
|
65
|
-
export declare function isUnderDir(targetPath: string, baseDir: string): boolean;
|
|
66
100
|
/** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath.
|
|
67
101
|
* A non-existent path can't be realpath'd, so it stays as the plain absolute resolve. */
|
|
68
102
|
export declare function canonicalPath(p: string): string;
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
* (bad skill files, name collisions) are returned as data. An unreadable ② context file only warns (pi).
|
|
15
15
|
*/
|
|
16
16
|
import { realpathSync } from "node:fs";
|
|
17
|
-
import {
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
18
|
import { 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
|
+
import { log } from "../../log.js";
|
|
21
22
|
import { assertInsideAgentDir } from "../../paths.js";
|
|
22
23
|
/** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
|
|
23
24
|
export async function loadAgentDefinition(agentDir, options = {}) {
|
|
@@ -45,6 +46,119 @@ export async function loadAgentDefinition(agentDir, options = {}) {
|
|
|
45
46
|
const { skills, diagnostics, collisions } = await readSkills(e, root);
|
|
46
47
|
return { contextFiles, persona, skills, diagnostics, collisions, dir: root };
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Extension entry-point FILES under `<agentDir>/extensions/`, empty when there are none.
|
|
51
|
+
*
|
|
52
|
+
* Paths, not loaded objects — unlike skills (data: content inline, serializable), an extension is
|
|
53
|
+
* CODE that pi loads with jiti and binds to its own eventBus/runtime, so loading it here would
|
|
54
|
+
* reimplement pi's loader. The engine binding hands these to pi as `additionalExtensionPaths`, which
|
|
55
|
+
* survives `noExtensions: true` — that flag suppresses MACHINE-GLOBAL discovery (`~/.pi`), and the
|
|
56
|
+
* definition's own extensions were only ever collateral to it. FILES, not the directory: pi's paths
|
|
57
|
+
* are module specifiers, and a directory fails as `Cannot find module` into a
|
|
58
|
+
* `LoadExtensionsResult.errors` entry nothing reads.
|
|
59
|
+
*
|
|
60
|
+
* SEPARATE from {@link loadAgentDefinition} on purpose. Prompt and skills are re-read every invoke
|
|
61
|
+
* ("the directory is the agent, LIVE"); scanning for extension entry points is not, since the set
|
|
62
|
+
* cannot change without a restart. Called once per assembly, like `tools/`.
|
|
63
|
+
*
|
|
64
|
+
* Discovery runs on BOTH paths; loading does not. `chat` hands these to pi and runs them fully.
|
|
65
|
+
* Serving only announces them — pi's extension runtime is shared across sessions, and serving has
|
|
66
|
+
* concurrent turns for unrelated conversations (see `PiAgentSessionFactoryOptions.extensionPaths`).
|
|
67
|
+
* The refusals below are therefore about what the ARTIFACT may contain, and hold for both.
|
|
68
|
+
*
|
|
69
|
+
* Discovery follows pi's own rules, so an extension that works in pi works here:
|
|
70
|
+
*
|
|
71
|
+
* 1. a direct `*.ts` / `*.js` file;
|
|
72
|
+
* 2. a subdirectory with `index.ts` / `index.js`.
|
|
73
|
+
*
|
|
74
|
+
* pi has a third rule — a subdirectory whose `package.json` declares a `pi` field — which is NOT
|
|
75
|
+
* implemented here. That shape is reported rather than skipped: a definition whose extension silently
|
|
76
|
+
* fails to load is the failure mode this whole path exists to remove.
|
|
77
|
+
*
|
|
78
|
+
* Containment matches skills/tools/channels/schedules (the fifth of five surfaces): a symlinked
|
|
79
|
+
* `extensions/` escaping the agent dir is refused, and — like `loadModuleDir`, whose `entry.isFile()`
|
|
80
|
+
* excludes them — a symlinked ENTRY is not loaded either. pi's own discovery does follow those, but
|
|
81
|
+
* an extension reached through a link out of the definition is code the artifact does not carry: it
|
|
82
|
+
* resolves on the authoring machine and is missing in the container. Refused loudly, never silently.
|
|
83
|
+
*
|
|
84
|
+
* Absent is normal and silent; a FILE at that path is not — that is an author who meant something.
|
|
85
|
+
*/
|
|
86
|
+
export async function loadExtensionPaths(agentDir, options = {}) {
|
|
87
|
+
const cwd = options.cwd ?? agentDir;
|
|
88
|
+
const e = options.env ?? new NodeExecutionEnv({ cwd });
|
|
89
|
+
const rootResult = await e.absolutePath(agentDir);
|
|
90
|
+
if (!rootResult.ok)
|
|
91
|
+
throw new Error(`cannot resolve agent dir "${agentDir}": ${rootResult.error.message}`);
|
|
92
|
+
const root = rootResult.value;
|
|
93
|
+
await assertInsideAgentDir(root, "extensions");
|
|
94
|
+
const dir = join(root, "extensions");
|
|
95
|
+
const listed = await e.listDir(dir);
|
|
96
|
+
if (!listed.ok) {
|
|
97
|
+
if (listed.error.code === "not_found")
|
|
98
|
+
return [];
|
|
99
|
+
throw new Error(`cannot read ${dir}: ${listed.error.message}`);
|
|
100
|
+
}
|
|
101
|
+
const paths = [];
|
|
102
|
+
for (const entry of listed.value) {
|
|
103
|
+
if (entry.kind === "symlink") {
|
|
104
|
+
// EVERY symlink here is announced, without guessing whether it meant to be an extension. The
|
|
105
|
+
// two mistakes are not equal: a needless line about a symlinked README costs a glance, while
|
|
106
|
+
// staying quiet about a symlinked extension loses a feature silently and only shows up in the
|
|
107
|
+
// container. A name-based guess also cannot see through the link — `audit.ext -> some/dir` is
|
|
108
|
+
// a directory candidate to pi and a mystery here — so the warning says what it knows and
|
|
109
|
+
// tells the author when to ignore it.
|
|
110
|
+
warnSymlinkRefused(entry.path);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
|
|
114
|
+
if (entry.kind === "file")
|
|
115
|
+
paths.push(entry.path);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (entry.kind === "file")
|
|
119
|
+
continue; // a README, a .json — not an extension, not a problem
|
|
120
|
+
const index = await firstRealFile(e, [join(entry.path, "index.ts"), join(entry.path, "index.js")]);
|
|
121
|
+
if (index.path) {
|
|
122
|
+
paths.push(index.path);
|
|
123
|
+
}
|
|
124
|
+
else if (!index.refused) {
|
|
125
|
+
// Silent when the index WAS found and refused for being a symlink: that warning already named
|
|
126
|
+
// the real problem, and "expected index.ts" on top of it describes a directory that has one.
|
|
127
|
+
log.warn(`[fastagent] ${entry.path} is not a loadable extension: expected index.ts or index.js ` +
|
|
128
|
+
`(pi's package.json "pi" manifest form is not supported here) — it will not be loaded`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return paths.sort();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The first candidate that is a REAL file. `exists` would follow a symlink, which is how a
|
|
135
|
+
* subdirectory's `index.ts` could otherwise point outside the definition and slip past the rule the
|
|
136
|
+
* top-level entries already follow.
|
|
137
|
+
*/
|
|
138
|
+
/** The first real file among the candidates, and whether one was found but REFUSED as a symlink. */
|
|
139
|
+
async function firstRealFile(e, candidates) {
|
|
140
|
+
let refused = false;
|
|
141
|
+
for (const candidate of candidates) {
|
|
142
|
+
const info = await e.fileInfo(candidate);
|
|
143
|
+
if (!info.ok) {
|
|
144
|
+
if (info.error.code === "not_found")
|
|
145
|
+
continue;
|
|
146
|
+
throw new Error(`cannot read ${candidate}: ${info.error.message}`);
|
|
147
|
+
}
|
|
148
|
+
if (info.value.kind === "file")
|
|
149
|
+
return { path: candidate, refused };
|
|
150
|
+
if (info.value.kind === "symlink") {
|
|
151
|
+
warnSymlinkRefused(candidate);
|
|
152
|
+
refused = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { refused };
|
|
156
|
+
}
|
|
157
|
+
function warnSymlinkRefused(path) {
|
|
158
|
+
log.warn(`[fastagent] ${path} is a symlink and will not be loaded: an extension must be a real file inside ` +
|
|
159
|
+
`the definition so it travels with the artifact — move it in. (If it is not an extension, ` +
|
|
160
|
+
`keep it outside extensions/ to silence this.)`);
|
|
161
|
+
}
|
|
48
162
|
/** The skills half, shared by the full load and {@link loadAgentSkills}. `root` is already resolved. */
|
|
49
163
|
async function readSkills(e, root) {
|
|
50
164
|
// Skills come ONLY from the definition's own skills/ (no external/global mount), so the same
|
|
@@ -81,16 +195,6 @@ export async function loadAgentSkills(agentDir, options = {}) {
|
|
|
81
195
|
// (the findings memo) on it, and "./agent" vs an absolute path must not become two definitions.
|
|
82
196
|
return { ...(await readSkills(e, rootResult.value)), dir: rootResult.value };
|
|
83
197
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
|
|
86
|
-
* this OUT of the agent?" — the startup report's redeploy notes, `add`'s printed `.env` label, and the
|
|
87
|
-
* dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
|
|
88
|
-
* does not act on where a user's paths point.
|
|
89
|
-
*/
|
|
90
|
-
export function isUnderDir(targetPath, baseDir) {
|
|
91
|
-
const rel = relative(baseDir, targetPath);
|
|
92
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
93
|
-
}
|
|
94
198
|
/** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath.
|
|
95
199
|
* A non-existent path can't be realpath'd, so it stays as the plain absolute resolve. */
|
|
96
200
|
export function canonicalPath(p) {
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* L0
|
|
3
|
-
*
|
|
4
|
-
* session
|
|
2
|
+
* THE L0: pi's `AgentSession`, one per invoke, over the same durable record —
|
|
3
|
+
* [conformance-levels.md](../../../docs/design/conformance-levels.md) §2's `per-invoke` posture.
|
|
4
|
+
* Build a session, run one turn, dispose; continuity lives in the record, never in this process.
|
|
5
5
|
*
|
|
6
|
-
* Why this
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* on the class pi actually maintains (test/conformance-session.test.ts).
|
|
6
|
+
* Why this class: pi 0.84 replaced `AgentHarness` with an unimplemented lane-based skeleton, and pi
|
|
7
|
+
* does not consume that class itself — its TUI, RPC and SDK all run on `AgentSession`. Being the
|
|
8
|
+
* sole consumer of a surface nobody dogfoods is a position, not an architecture.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Events are translated ONCE, into the rich `SessionEvent` vocabulary the observation plane speaks;
|
|
11
|
+
* the SPEC stream is a projection of that (`docs/design/session-control.md` §6 — one translation
|
|
12
|
+
* plus one projection, never two parallel ones).
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
20
|
-
* with it, and the harness L0 goes away.
|
|
14
|
+
* Two disciplines this file exists to hold:
|
|
15
|
+
* - the turn's outcome comes from the EVENT STREAM, never from an index into session state, which
|
|
16
|
+
* compaction and overflow recovery both rewrite mid-turn;
|
|
17
|
+
* - `run_started` is published BEFORE the session is bound, so a dispatch racing the build queues on
|
|
18
|
+
* the run's controls instead of finding no run.
|
|
21
19
|
*/
|
|
22
20
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import type { SessionInheritance } from "./session-inheritance.ts";
|
|
23
22
|
import { type Agent } from "../../agent.ts";
|
|
24
|
-
import { type Lease } from "./turn-kit.ts";
|
|
25
|
-
/** Open-or-create the session behind `sessionId` and bind an `AgentSession` to it, per invoke.
|
|
26
|
-
|
|
23
|
+
import { type Lease, type SessionObserver } from "./turn-kit.ts";
|
|
24
|
+
/** Open-or-create the session behind `sessionId` and bind an `AgentSession` to it, per invoke.
|
|
25
|
+
* `inherit` reaches the CREATE path only — an existing session ignores it. */
|
|
26
|
+
export type PiAgentSessionFactory = (sessionId: string, inherit?: SessionInheritance) => Promise<AgentSession>;
|
|
27
27
|
export interface CreatePiAgentFromSessionOptions {
|
|
28
28
|
sessionFactory: PiAgentSessionFactory;
|
|
29
29
|
/** Single-writer lease. Defaults to the in-process per-session fail-fast lease. */
|
|
30
30
|
lease?: Lease;
|
|
31
|
+
/** Observation-plane tap: every rich event of every run, plus the run's live {@link RunControls}
|
|
32
|
+
* on `run_started`. Optional; the SPEC stream is identical with or without it. */
|
|
33
|
+
observer?: SessionObserver;
|
|
31
34
|
}
|
|
32
35
|
export declare function createPiAgentFromSession(options: CreatePiAgentFromSessionOptions): Agent;
|