@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.
Files changed (212) hide show
  1. package/README.md +5 -1
  2. package/dist/atomic-write.d.ts +11 -7
  3. package/dist/atomic-write.js +16 -11
  4. package/dist/channels/agentcore-limits.d.ts +9 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-protocol.d.ts +112 -0
  7. package/dist/channels/agentcore-protocol.js +22 -0
  8. package/dist/channels/agentcore-service.d.ts +12 -15
  9. package/dist/channels/agentcore-service.js +15 -24
  10. package/dist/channels/agentcore-state.d.ts +5 -11
  11. package/dist/channels/agentcore-state.js +4 -1
  12. package/dist/channels/agentcore.d.ts +9 -83
  13. package/dist/channels/agentcore.js +101 -93
  14. package/dist/channels/control.d.ts +50 -12
  15. package/dist/channels/control.js +251 -157
  16. package/dist/channels/discover.d.ts +26 -5
  17. package/dist/channels/discover.js +13 -26
  18. package/dist/channels/feishu/context-buffer.d.ts +6 -0
  19. package/dist/channels/feishu/context-buffer.js +0 -38
  20. package/dist/channels/feishu/crypto.d.ts +0 -2
  21. package/dist/channels/feishu/crypto.js +3 -8
  22. package/dist/channels/feishu/feishu-api.js +7 -12
  23. package/dist/channels/feishu/feishu.js +86 -128
  24. package/dist/channels/feishu/parse.d.ts +4 -3
  25. package/dist/channels/feishu/parse.js +5 -4
  26. package/dist/channels/feishu/register-webhook.d.ts +5 -5
  27. package/dist/channels/feishu/register-webhook.js +43 -57
  28. package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
  29. package/dist/channels/feishu/setup-mode.d.ts +30 -0
  30. package/dist/channels/feishu/setup-mode.js +26 -0
  31. package/dist/channels/feishu/shared-api.d.ts +10 -0
  32. package/dist/channels/feishu/shared-api.js +38 -0
  33. package/dist/channels/http.d.ts +0 -8
  34. package/dist/channels/http.js +4 -56
  35. package/dist/channels/kit/attachment-path.d.ts +12 -0
  36. package/dist/channels/kit/attachment-path.js +43 -0
  37. package/dist/channels/kit/context-buffer.d.ts +9 -0
  38. package/dist/channels/kit/context-buffer.js +11 -0
  39. package/dist/channels/kit/signature.d.ts +12 -0
  40. package/dist/channels/kit/signature.js +17 -0
  41. package/dist/channels/kit/stop-command.js +3 -3
  42. package/dist/channels/kit/tasks.d.ts +1 -1
  43. package/dist/channels/kit/tasks.js +10 -4
  44. package/dist/channels/kit/turn-runner.d.ts +59 -0
  45. package/dist/channels/kit/turn-runner.js +84 -0
  46. package/dist/channels/kit/turn-store.d.ts +19 -3
  47. package/dist/channels/kit/turn-store.js +29 -2
  48. package/dist/channels/lark/scaffold/lark-send.ts +12 -67
  49. package/dist/channels/registration.d.ts +36 -1
  50. package/dist/channels/registration.js +57 -1
  51. package/dist/channels/secret.d.ts +1 -0
  52. package/dist/channels/secret.js +16 -0
  53. package/dist/channels/slack/config-api.d.ts +35 -3
  54. package/dist/channels/slack/config-api.js +51 -9
  55. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  56. package/dist/channels/slack/invoke-turn.js +5 -1
  57. package/dist/channels/slack/manifest.js +5 -1
  58. package/dist/channels/slack/onboard.d.ts +5 -5
  59. package/dist/channels/slack/onboard.js +44 -23
  60. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  61. package/dist/channels/slack/onboarding-state.js +13 -20
  62. package/dist/channels/slack/register-webhook.d.ts +3 -2
  63. package/dist/channels/slack/register-webhook.js +35 -15
  64. package/dist/channels/slack/scaffold/channel.ts +3 -10
  65. package/dist/channels/slack/scaffold/slack-send.ts +18 -141
  66. package/dist/channels/slack/shared-api.d.ts +10 -0
  67. package/dist/channels/slack/shared-api.js +34 -0
  68. package/dist/channels/slack/slack-api.d.ts +20 -2
  69. package/dist/channels/slack/slack-api.js +100 -73
  70. package/dist/channels/slack/slack.d.ts +0 -10
  71. package/dist/channels/slack/slack.js +68 -107
  72. package/dist/channels/sse.d.ts +4 -0
  73. package/dist/channels/sse.js +66 -0
  74. package/dist/channels/telegram/parse.d.ts +21 -1
  75. package/dist/channels/telegram/parse.js +65 -11
  76. package/dist/channels/telegram/register-webhook.d.ts +6 -9
  77. package/dist/channels/telegram/register-webhook.js +44 -42
  78. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  79. package/dist/channels/telegram/telegram-api.js +5 -6
  80. package/dist/channels/telegram/telegram.d.ts +2 -2
  81. package/dist/channels/telegram/telegram.js +97 -204
  82. package/dist/channels/wait-health.js +7 -4
  83. package/dist/cli/add-feishu.js +3 -10
  84. package/dist/cli/add-slack.js +13 -25
  85. package/dist/cli/commands/add.d.ts +0 -1
  86. package/dist/cli/commands/add.js +22 -35
  87. package/dist/cli/commands/attach.d.ts +2 -4
  88. package/dist/cli/commands/attach.js +17 -15
  89. package/dist/cli/commands/chat.js +6 -12
  90. package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
  91. package/dist/cli/commands/deploy/agentcore.js +178 -0
  92. package/dist/cli/commands/deploy/docker.d.ts +2 -0
  93. package/dist/cli/commands/deploy/docker.js +119 -0
  94. package/dist/cli/commands/deploy/fly.d.ts +2 -0
  95. package/dist/cli/commands/deploy/fly.js +131 -0
  96. package/dist/cli/commands/deploy/railway.d.ts +2 -0
  97. package/dist/cli/commands/deploy/railway.js +71 -0
  98. package/dist/cli/commands/deploy/shared.d.ts +114 -0
  99. package/dist/cli/commands/deploy/shared.js +124 -0
  100. package/dist/cli/commands/deploy.d.ts +38 -25
  101. package/dist/cli/commands/deploy.js +101 -631
  102. package/dist/cli/commands/dev.js +20 -76
  103. package/dist/cli/commands/fire.js +6 -12
  104. package/dist/cli/commands/info.js +1 -1
  105. package/dist/cli/commands/init.js +1 -2
  106. package/dist/cli/commands/invoke.js +4 -11
  107. package/dist/cli/commands/schedule.js +1 -1
  108. package/dist/cli/commands/start.js +41 -80
  109. package/dist/cli/commands/tool.js +3 -8
  110. package/dist/cli/kernel.d.ts +0 -2
  111. package/dist/cli/kernel.js +0 -2
  112. package/dist/cli/program.js +7 -12
  113. package/dist/cli/serve.d.ts +42 -34
  114. package/dist/cli/serve.js +107 -28
  115. package/dist/cli/shared.d.ts +52 -27
  116. package/dist/cli/shared.js +69 -6
  117. package/dist/deploy/agentcore/forwarder.js +250 -0
  118. package/dist/deploy/agentcore/logs.d.ts +2 -2
  119. package/dist/deploy/agentcore/logs.js +2 -2
  120. package/dist/deploy/agentcore/plan.d.ts +34 -11
  121. package/dist/deploy/agentcore/plan.js +52 -259
  122. package/dist/deploy/agentcore/run.d.ts +9 -24
  123. package/dist/deploy/agentcore/run.js +35 -43
  124. package/dist/deploy/channel-ingress.d.ts +73 -0
  125. package/dist/deploy/channel-ingress.js +101 -0
  126. package/dist/deploy/docker/plan.d.ts +3 -7
  127. package/dist/deploy/docker/plan.js +4 -15
  128. package/dist/deploy/docker/run.d.ts +35 -4
  129. package/dist/deploy/docker/run.js +40 -8
  130. package/dist/deploy/fly/plan.d.ts +4 -5
  131. package/dist/deploy/fly/plan.js +14 -22
  132. package/dist/deploy/fly/run.d.ts +36 -23
  133. package/dist/deploy/fly/run.js +129 -83
  134. package/dist/deploy/hosts.d.ts +5 -0
  135. package/dist/deploy/hosts.js +4 -0
  136. package/dist/deploy/preflight.d.ts +6 -7
  137. package/dist/deploy/preflight.js +27 -23
  138. package/dist/deploy/railway/plan.d.ts +9 -5
  139. package/dist/deploy/railway/plan.js +18 -26
  140. package/dist/deploy/railway/run.d.ts +11 -10
  141. package/dist/deploy/railway/run.js +44 -43
  142. package/dist/deploy/runner.js +22 -2
  143. package/dist/deploy/secrets.d.ts +18 -9
  144. package/dist/deploy/secrets.js +63 -28
  145. package/dist/dev-supervisor.js +3 -4
  146. package/dist/engines/pi/agent-session-factory.d.ts +37 -17
  147. package/dist/engines/pi/agent-session-factory.js +111 -106
  148. package/dist/engines/pi/auth.js +43 -43
  149. package/dist/engines/pi/config.d.ts +2 -2
  150. package/dist/engines/pi/create.d.ts +22 -27
  151. package/dist/engines/pi/create.js +59 -74
  152. package/dist/engines/pi/definition.d.ts +1 -1
  153. package/dist/engines/pi/definition.js +11 -12
  154. package/dist/engines/pi/invoke-session.js +13 -30
  155. package/dist/engines/pi/login.js +32 -16
  156. package/dist/engines/pi/open.d.ts +3 -4
  157. package/dist/engines/pi/open.js +32 -42
  158. package/dist/engines/pi/retry-event.d.ts +6 -0
  159. package/dist/engines/pi/retry-event.js +15 -0
  160. package/dist/engines/pi/search-tools.js +1 -1
  161. package/dist/engines/pi/service.d.ts +1 -1
  162. package/dist/engines/pi/service.js +8 -0
  163. package/dist/engines/pi/session-builder.js +26 -140
  164. package/dist/engines/pi/session-control.d.ts +27 -22
  165. package/dist/engines/pi/session-control.js +551 -490
  166. package/dist/engines/pi/session-inheritance.d.ts +8 -22
  167. package/dist/engines/pi/session-inheritance.js +95 -76
  168. package/dist/engines/pi/session-markers.d.ts +48 -0
  169. package/dist/engines/pi/session-markers.js +59 -0
  170. package/dist/engines/pi/session-settings.d.ts +5 -5
  171. package/dist/engines/pi/session-settings.js +8 -5
  172. package/dist/engines/pi/session-store.d.ts +91 -26
  173. package/dist/engines/pi/session-store.js +413 -82
  174. package/dist/engines/pi/tool-context.d.ts +24 -11
  175. package/dist/engines/pi/tool-context.js +29 -4
  176. package/dist/engines/pi/tool.d.ts +6 -9
  177. package/dist/engines/pi/tool.js +3 -2
  178. package/dist/env.js +1 -2
  179. package/dist/feishu.d.ts +1 -0
  180. package/dist/feishu.js +1 -0
  181. package/dist/lark.d.ts +1 -0
  182. package/dist/lark.js +1 -0
  183. package/dist/loader.d.ts +51 -7
  184. package/dist/loader.js +84 -18
  185. package/dist/log.d.ts +9 -17
  186. package/dist/log.js +25 -30
  187. package/dist/paths.d.ts +26 -3
  188. package/dist/paths.js +43 -5
  189. package/dist/scaffold/add-channel.d.ts +6 -1
  190. package/dist/scaffold/add-channel.js +50 -67
  191. package/dist/scaffold/init.js +7 -2
  192. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  193. package/dist/schedule/discover.js +3 -15
  194. package/dist/schedule/wake-alarm.d.ts +14 -19
  195. package/dist/schedule/wake-alarm.js +89 -48
  196. package/dist/schedule/wakeups.d.ts +1 -1
  197. package/dist/schedule/wakeups.js +10 -7
  198. package/dist/service.d.ts +24 -24
  199. package/dist/service.js +33 -77
  200. package/dist/session-remote.d.ts +12 -7
  201. package/dist/session-remote.js +185 -118
  202. package/dist/session.d.ts +227 -93
  203. package/dist/session.js +61 -23
  204. package/dist/slack.d.ts +2 -0
  205. package/dist/slack.js +1 -0
  206. package/dist/telegram.d.ts +1 -1
  207. package/dist/telegram.js +1 -1
  208. package/dist/tunnel.d.ts +34 -9
  209. package/dist/tunnel.js +83 -50
  210. package/package.json +5 -4
  211. package/dist/channels/slack/bot-auth.d.ts +0 -15
  212. package/dist/channels/slack/bot-auth.js +0 -135
@@ -124,7 +124,7 @@ export function makeSearchToolsTool() {
124
124
  .map((t) => t.name)
125
125
  .join(", ")}${more > 0 ? ` … and ${more} more` : ""}.${activeNote ? ` ${activeNote}` : ""}`;
126
126
  }
127
- const activated = await ctx.tools.activate(inactiveMatches.map((t) => t.name));
127
+ const activated = ctx.tools.activate(inactiveMatches.map((t) => t.name));
128
128
  // Report what actually happened, not what was attempted: a parallel sibling call may have
129
129
  // activated the same matches first, leaving nothing new here — an empty "Activated:" would lie.
130
130
  if (activated.length === 0) {
@@ -6,7 +6,7 @@
6
6
  * pi-specific is opening a DIRECTORY into one, which is why this shortcut lives here and the
7
7
  * assembly does not. A second engine ships its own opener and reuses `mountAgentService` unchanged.
8
8
  */
9
- import type { AgentService, MountAgentServiceOptions } from "../../service.ts";
9
+ import { type AgentService, type MountAgentServiceOptions } from "../../service.ts";
10
10
  export interface CreateAgentServiceOptions extends MountAgentServiceOptions {
11
11
  model?: string;
12
12
  authPath?: string;
@@ -1,3 +1,11 @@
1
+ /**
2
+ * `createAgentService` — the product as one call, with pi supplying the engine.
3
+ *
4
+ * The ASSEMBLY is engine-neutral and lives in `src/service.ts`: it takes a {@link MountableAgent}
5
+ * (the SPEC contract plus three paths) and knows nothing about how that agent was built. What is
6
+ * pi-specific is opening a DIRECTORY into one, which is why this shortcut lives here and the
7
+ * assembly does not. A second engine ships its own opener and reuses `mountAgentService` unchanged.
8
+ */
1
9
  import { mountAgentService } from "../../service.js";
2
10
  import { createPiAgentFromDir } from "./open.js";
3
11
  /**
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * The shared definition-aware session builder: open a directory's assembled agent as a resident pi
3
- * `AgentSessionRuntime`. Extracted from chat.ts (session-control Phase 0) as the proof of the
4
- * assembly seam independently instantiable, running the SAME agent that `dev`/`start` serve. The
5
- * TUI (chat.ts) is its one consumer: the session control plane (Phases 1–3) was built on the invoke
6
- * pipeline instead of this resident runtime, so control-plane observation covers invoke-driven runs
7
- * and deliberately not chat sessions (design §10/§15).
3
+ * `AgentSessionRuntime`, running the SAME agent that `dev`/`start` serve. The TUI (chat.ts) is its
4
+ * one consumer: the session control plane was built on the invoke pipeline instead of this resident
5
+ * runtime, so control-plane observation covers invoke-driven runs and deliberately not chat sessions
6
+ * (docs/design/session-control.md §10, §15).
8
7
  *
9
8
  * FIDELITY: pi's vanilla discovery (AGENTS.md walk to repo root, machine-global skills/extensions)
10
9
  * is suppressed; fastagent's assembly is INJECTED into pi's session:
@@ -28,18 +27,16 @@
28
27
  * workspace.
29
28
  */
30
29
  import { existsSync, readFileSync } from "node:fs";
31
- import { join, resolve } from "node:path";
30
+ import { resolve } from "node:path";
32
31
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
33
- import { SessionManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, } from "@earendil-works/pi-coding-agent";
34
- import { definitionResourceLoaderOptions, reportExtensionErrors } from "./agent-session-factory.js";
32
+ import { SessionManager, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, } from "@earendil-works/pi-coding-agent";
33
+ import { bindPiSession, definitionResourceLoaderOptions, reportExtensionErrors } from "./agent-session-factory.js";
35
34
  import { resolveModel } from "./config.js";
36
35
  import { assembleSystemPrompt, piBasePrompt } from "./create.js";
37
36
  import { canonicalPath, loadAgentDefinition, loadExtensionPaths } from "./definition.js";
38
- import { createPiModelRuntime, probeAuthSource } from "./models.js";
39
- import { log } from "../../log.js";
40
- import { additiveActivation, agentSessionManager, turnContext, } from "./tool-context.js";
37
+ import { createPiModelRuntime } from "./models.js";
38
+ import { reportModuleLoadFailures } from "../../loader.js";
41
39
  import { reportFindingsIfChanged, reportToolCollisions } from "./report.js";
42
- import { reportModuleLoadFailures } from "../../log.js";
43
40
  import { resolveAgentAssembly } from "./open.js";
44
41
  /**
45
42
  * Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
@@ -49,45 +46,12 @@ import { resolveAgentAssembly } from "./open.js";
49
46
  export async function buildAgentSessionRuntime(dir, options = {},
50
47
  /** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
51
48
  sessionManager) {
52
- /** The turn's {@link ToolActivation} over pi's AgentSession — the counterpart of invoke.ts's
53
- * serving bridge, so the SAME builtin search_tools serves both paths. Additive; unknown names
54
- * filtered (`setActiveToolsByName` is authoritative on the session and rebuilds its prompt — our
55
- * static override keeps the prompt identical to serving). */
56
- function sessionToolActivation(session) {
57
- // Same serialization as invoke.ts's bridge (there per turn; here per session — interactive turns
58
- // make per-session equivalent): the read-modify-write below is only race-free while nothing awaits
59
- // between read and write, and pi's session setters happening to be synchronous today is not a
60
- // contract worth betting parallel tool batches on. Built ONCE per session (createRuntime), so
61
- // parallel calls actually share the chain.
62
- let chain = Promise.resolve([]);
63
- return {
64
- active: () => session.getActiveToolNames(),
65
- registered: () => session.getAllTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
66
- activate(names) {
67
- const run = async () => {
68
- const current = session.getActiveToolNames();
69
- const added = additiveActivation(session.getAllTools().map((t) => t.name), current, names);
70
- if (added.length > 0)
71
- session.setActiveToolsByName([...current, ...added]);
72
- return added;
73
- };
74
- const result = chain.then(run, run); // run after the predecessor settles, success or failure
75
- chain = result.catch(() => []); // the caller sees a rejection on `result`; the chain stays usable
76
- return result;
77
- },
78
- };
79
- }
80
49
  async function resolveAssembly(cwd) {
81
50
  // The shared front half — the SAME placement/config/model-spec/tool/auth resolution the serving
82
51
  // opener uses (open.ts); those inputs cannot drift between the two consumption shapes.
83
- // (Definition→prompt assembly is NOT shared: serving re-reads live per invoke, this runtime is a
84
- // startup snapshot and pi appends skills/env itself — see the header.) `tools` arrives with
85
- // search_tools applied; deferral is EMULATED below like serving
86
- // (what you iterate is what you serve): the initial active set excludes deferred tools (applied
87
- // on the session in createRuntime — pi's session starts all-active), and the activation bridge
88
- // above rides the same turn context, so the SAME search_tools works against pi's AgentSession
89
- // instead of the served one.
90
- const { config, modelSpec, agentDir, authPath, stateRoot, tools, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
52
+ // Serving re-reads the definition per invoke; chat keeps a startup snapshot. Both pass identity
53
+ // and project context to Pi, which appends skills/cwd. `tools` already includes search_tools.
54
+ const { config, modelSpec, agentDir, authPath, stateRoot, tools, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
91
55
  reportToolCollisions(toolCollisions);
92
56
  reportModuleLoadFailures(toolFailures);
93
57
  // ONE hub owns model resolution AND per-request auth; see models.ts.
@@ -100,33 +64,6 @@ sessionManager) {
100
64
  // Assembly-time, like serving's: this whole function is memoized, so the scan and its warnings
101
65
  // happen once per runtime rather than per session rebuild (/new, /resume, fork).
102
66
  const extensionPaths = await loadExtensionPaths(agentDir, { cwd, env });
103
- // fastagent mounts pi's complete coding set itself; `noTools: "builtin"` below keeps the runtime
104
- // from adding duplicate copies.
105
- // Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
106
- // it). Each execute runs inside the turn context with the CURRENT session's activation bridge — the
107
- // assembly is memoized across /new//resume/fork rebuilds while the session changes, so the bridge
108
- // resolves through sessionRef at call time, exactly like the serving path resolves its session.
109
- const customToolDefs = tools.map((t) => ({
110
- name: t.name,
111
- label: t.name,
112
- description: t.description ?? "",
113
- parameters: t.parameters,
114
- // Propagate the execution mode — an activating tool (the builtin loader) declares "sequential"
115
- // so pi serializes its batch; without this, pi's outer active-set diff double-stamps parallels.
116
- executionMode: t.executionMode,
117
- execute: (id, params, signal) => {
118
- const bound = sessionRef.current;
119
- // Unreachable by construction (createRuntime sets sessionRef before any turn can run a tool).
120
- // Throw rather than silently run outside the turn context — that would disguise a broken
121
- // session-lifecycle invariant as a normal out-of-turn call (fail visibly).
122
- if (!bound)
123
- throw new Error("tool executed before its session was built (lifecycle invariant broken)");
124
- return turnContext.run({ cwd, sessionManager: bound.sessionManager, tools: bound.activation },
125
- // Lower-level MountedTools may consume the fifth-argument env. Directory coding tools are
126
- // cwd-bound and ignore it; authored tools read FastAgent's turnContext instead.
127
- () => t.execute(id, params, signal, undefined, { env }));
128
- },
129
- }));
130
67
  // base + instructions ONLY — pi appends the skill section and env (cwd) itself (including
131
68
  // them here would duplicate them).
132
69
  const systemPrompt = assembleSystemPrompt({
@@ -136,14 +73,11 @@ sessionManager) {
136
73
  return {
137
74
  modelRuntime,
138
75
  modelSpec,
139
- authPath,
140
76
  // Serving honors config.thinkingLevel (config → L2); the resident session must too (fidelity).
141
77
  thinkingLevel: config.thinkingLevel,
142
78
  definition,
143
79
  extensionPaths,
144
- customTools: tools,
145
- customToolDefs,
146
- deferredToolNames,
80
+ tools,
147
81
  systemPrompt,
148
82
  };
149
83
  }
@@ -153,13 +87,6 @@ sessionManager) {
153
87
  // load edits. And keep it workspace-scoped — `.env` is process-global, so a switch to another cwd
154
88
  // would leak env or require mutating global env at runtime.
155
89
  const rootCwd = canonicalPath(dir);
156
- // The CURRENT pi session + its activation bridge, BOUND TOGETHER — rebuilt on /new//resume/fork
157
- // while the memoized assembly (and its tool execute closures) stays. The bridge must share the
158
- // session's lifetime, NOT be rebuilt per tool call (a per-call chain serializes nothing). Note on
159
- // parallel batches: pi wraps SDK customTools in its own before/after active-set diff, so an
160
- // activating tool must carry `executionMode: "sequential"` (the builtin loader does) — pi then runs
161
- // the whole batch serially and the outer diff sees correct snapshots.
162
- const sessionRef = {};
163
90
  let assembly;
164
91
  const assemblyFor = (cwd) => {
165
92
  // Canonical paths: pi's process.cwd() fallback is a realpath, so a symlinked workspace would
@@ -171,12 +98,8 @@ sessionManager) {
171
98
  assembly ??= resolveAssembly(rootCwd);
172
99
  return assembly;
173
100
  };
174
- // The credential hint belongs to the RUNTIME, not to each session it builds: model resolution
175
- // moved into createRuntime (extensions must load first), and repeating this on every /new,
176
- // /resume and fork would nag about a setting that did not change.
177
- let credentialHintShown = false;
178
101
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
179
- const { modelRuntime, modelSpec, authPath, thinkingLevel, definition, extensionPaths, customToolDefs, deferredToolNames, systemPrompt, } = await assemblyFor(cwd);
102
+ const { modelRuntime, modelSpec, thinkingLevel, definition, extensionPaths, tools, systemPrompt } = await assemblyFor(cwd);
180
103
  // Per session, NOT memoized with the assembly: pi replaces the session on /new, /resume and
181
104
  // fork, and its extension contract is that the replacement gets freshly loaded extensions
182
105
  // rather than the previous session's objects. The expensive halves (model hub, auth) are shared
@@ -199,62 +122,25 @@ sessionManager) {
199
122
  // AFTER the services, because an extension may be what defines the model. `registerProvider()`
200
123
  // is pi's documented way for one to add providers, and extensions do not execute until the
201
124
  // services are built — resolving first fails a definition whose configured model comes from its
202
- // own extension with a bare "unknown model", after warning about credentials for a provider
203
- // that does not exist yet.
204
- //
205
- // MIGRATION HINT (deliberate breaking change): chat historically used pi's own `~/.pi` auth; it
206
- // now reads the agent's credential file like every other command. Probe the RESOLVED model's
207
- // provider through the normal resolution path (stored credential OR env var — an env-authed
208
- // user is fine and must not be warned): only when that provider has no usable auth AND pi's old
209
- // file exists does the bare provider error get its cause named.
210
- if (!credentialHintShown &&
211
- (await probeAuthSource(modelRuntime, modelSpec)) === undefined &&
212
- existsSync(join(getAgentDir(), "auth.json"))) {
213
- credentialHintShown = true;
214
- log.warn(`[fastagent] no credentials for ${modelSpec} in ${authPath} — this runtime no longer reads ` +
215
- `pi's ~/.pi auth; run \`fastagent login\` (or /login in the TUI) to store credentials for this agent`);
216
- }
125
+ // own extension with a bare "unknown model".
217
126
  const model = resolveModel(modelRuntime, modelSpec);
218
- const result = await createAgentSessionFromServices({
127
+ // The same bind serving performs, minus the activation record: pi's chat session has nowhere to
128
+ // put one, which is the documented divergence — a resumed chat re-discovers via search_tools.
129
+ // NOT bound to the host here: InteractiveMode.bindCurrentSessionExtensions() calls
130
+ // session.bindExtensions() with the TUI's uiContext, abort handler and command actions — binding
131
+ // here too would emit session_start twice per chat, so an extension opening a resource on start
132
+ // would open two.
133
+ const result = await bindPiSession({
219
134
  services,
220
135
  sessionManager,
221
136
  sessionStartEvent,
222
137
  model,
223
138
  thinkingLevel,
224
- // NO `tools` allowlist. It would freeze the tool set at build time, and pi lets an extension
225
- // register from `session_start`, a command, or any handler those names would not be in a
226
- // startup snapshot, so `refreshTools()` would filter them straight back out and the extension
227
- // would look like it did nothing. `noTools: "builtin"` gets the same guarantee the allowlist
228
- // was really there for (the machine's `defaultTools` setting cannot add pi's own copies on top
229
- // of ours) without freezing anything.
230
- noTools: "builtin",
231
- customTools: customToolDefs,
139
+ tools,
140
+ // A tool must see one spelling of the workspace, including when opened through a symlink.
141
+ cwd: rootCwd,
142
+ recordActivations: false,
232
143
  });
233
- sessionRef.current = {
234
- session: result.session,
235
- sessionManager: agentSessionManager(result.session, result.session.sessionManager.getSessionId()),
236
- activation: sessionToolActivation(result.session),
237
- };
238
- // NOT bound here: the HOST does it. InteractiveMode.bindCurrentSessionExtensions() calls
239
- // session.bindExtensions() with the TUI's uiContext, abort handler and command actions — binding
240
- // here too would emit session_start twice per chat, so an extension opening a resource on start
241
- // would open two.
242
- // Deferral emulation: pi starts THIS agent's tools active — every coding tool it mounted, plus
243
- // every custom and extension tool. So narrow by SUBTRACTING the deferred names from whatever is
244
- // active, rather than stating a set: an exact-set-equality gate would silently stop narrowing the
245
- // day pi activates one more.
246
- //
247
- // Applied on EVERY build including /resume: pi's chat session does not record activations (its
248
- // SessionContext has no activeToolNames), so "restore prior activations" is not implementable
249
- // here — deferral stays consistently ON and a resumed conversation re-discovers via search_tools
250
- // (a documented divergence from serving, where activations persist in the session). The deferred
251
- // SET comes from the shared assembly (one definition of "deferred"), never recomputed here.
252
- if (deferredToolNames.length > 0) {
253
- const active = result.session.getActiveToolNames();
254
- if (deferredToolNames.some((n) => active.includes(n))) {
255
- result.session.setActiveToolsByName(active.filter((n) => !deferredToolNames.includes(n)));
256
- }
257
- }
258
144
  return { ...result, services, diagnostics: services.diagnostics };
259
145
  };
260
146
  const runtime = await createAgentSessionRuntime(createRuntime, {
@@ -1,22 +1,30 @@
1
+ /**
2
+ * The pi implementation of the session control plane. `createPiSessionControl` returns the neutral
3
+ * `SessionControl` plus the {@link SessionObserver} to plug into the invoke pipeline
4
+ * (`createPiAgent({ observer })`).
5
+ *
6
+ * It holds no durable state of its own: live truth comes from the event stream (plus the
7
+ * {@link RunControls} a `run_started` carries), durable truth from {@link PiSessionRecordStore} —
8
+ * which is also what performs every write, so how a record takes a property is not knowledge this
9
+ * file has. What it owns is the vocabulary: capability gating, the lease, error codes, and the
10
+ * events its own writes emit.
11
+ *
12
+ * Writes take the same lease as runs. Without boundary wiring they reject before acceptance with
13
+ * `unsupported_capability` — a client gating on `capabilities()` never sends them.
14
+ */
1
15
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
- import type { Models } from "@earendil-works/pi-ai";
16
+ import { type Models } from "@earendil-works/pi-ai";
3
17
  import { type AgentCommand, type SessionControl, type SessionEvent } from "../../session.ts";
4
- import type { SessionObserver } from "./turn-kit.ts";
5
- import type { Lease } from "./turn-kit.ts";
18
+ import type { SessionObserver, Lease } from "./turn-kit.ts";
6
19
  import type { AnyModel } from "./models.ts";
7
20
  import type { PiAgentSessionFactory } from "./invoke-session.ts";
8
21
  import type { PiSessionRecordStore } from "./session-store.ts";
9
- /** Ceiling for one subscriber's unconsumed backlog. A consumer this far behind (a stalled remote
10
- * connection the wire's ReadableStream backpressure stops pulling while invokes keep pushing)
11
- * has its buffer FROZEN at the cap (memory bounded — the actual goal: ≈10k small events ≈ a few
12
- * MB worst case per stuck connection) and its subscription marked closed. The close is observed
13
- * via pulls — which a stalled connection by definition does not make — so a consumer that RESUMES
14
- * pulling first drains the frozen backlog, then gets done (no buffered event dropped), while a
15
- * permanently stalled one holds the frozen buffer until its TCP connection dies. Recovery either
16
- * way is the standard reconnect+backfill, semantically lossless. */
22
+ /** Per-subscriber backlog limits: event count and UTF-8 JSON bytes. Overflow freezes the accepted
23
+ * prefix; the reader drains it before seeing done, then reconnects and backfills via entries(). */
17
24
  export declare const SUBSCRIBER_BUFFER_CAP = 10000;
18
- /** What boundary mutations (compact / set_model / set_thinking / navigate) need — the SAME instances the
19
- * agent assembly uses: the lease (mutations must not race a run), the model registry (validation +
25
+ export declare const SUBSCRIBER_BUFFER_BYTES: number;
26
+ /** What the plane's writes (`update` / `compact` / `fork` / `delete`) need — the SAME instances the
27
+ * agent assembly uses: the lease (a write must not race a run), the model registry (validation +
20
28
  * allowedModels), and the session factory (compaction is a model call). Writes go through the
21
29
  * record the hub's reader opened — after an existence check, so the control plane never creates
22
30
  * a session (that is the data plane's monopoly). */
@@ -36,15 +44,12 @@ export interface PiBoundaryWiring {
36
44
  export interface CreatePiSessionControlOptions {
37
45
  /** Read-only access to the durable session records (the same root the agent writes). */
38
46
  sessions: PiSessionRecordStore;
39
- /** Boundary-mutation wiring, as a LAZY thunk: the hub's observer must exist before the agent
40
- * assembly that produces these parts, so the hub asks for them at dispatch time instead
41
- * (assembly completes before any dispatch can arrive). Absent / undefined → boundary commands
42
- * are gated off in `capabilities()` and rejected `unsupported_capability`. */
43
- boundary?: () => PiBoundaryWiring | undefined;
44
- /** The definition's names, as a LAZY thunk for the same reason {@link boundary} is one (the hub
45
- * exists before the assembly that can read a definition) — and async because the definition is
46
- * live: this must re-read it, not close over a boot snapshot, or `commands()` would advertise a
47
- * list the next turn no longer runs.
47
+ /** Boundary-mutation wiring the assembly's own parts. Absent boundary commands are gated off
48
+ * in `capabilities()` and rejected `unsupported_capability`. */
49
+ boundary?: PiBoundaryWiring;
50
+ /** The definition's names, as an async thunk because the definition is live: this must re-read it,
51
+ * not close over a boot snapshot, or `commands()` would advertise a list the next turn no longer
52
+ * runs.
48
53
  *
49
54
  * OPTIONAL because absence is a TRUE answer for the assembly that omits it: a hub over an L1
50
55
  * agent (`createPiAgent({ model, instructions, tools })`) has no definition and therefore no