@fastagent-sh/fastagent 0.17.1 → 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.
Files changed (170) hide show
  1. package/README.md +25 -7
  2. package/dist/agent.d.ts +12 -1
  3. package/dist/agent.js +1 -1
  4. package/dist/atomic-write.d.ts +19 -0
  5. package/dist/atomic-write.js +48 -0
  6. package/dist/{host/node.d.ts → channel.d.ts} +24 -28
  7. package/dist/channel.js +1 -0
  8. package/dist/channels/agentcore-service.d.ts +54 -0
  9. package/dist/channels/agentcore-service.js +111 -0
  10. package/dist/channels/agentcore.d.ts +12 -2
  11. package/dist/channels/agentcore.js +6 -4
  12. package/dist/channels/busy.d.ts +1 -1
  13. package/dist/channels/busy.js +1 -1
  14. package/dist/channels/control.d.ts +16 -6
  15. package/dist/channels/control.js +106 -4
  16. package/dist/{engines/pi/channel.d.ts → channels/discover.d.ts} +2 -2
  17. package/dist/{engines/pi/channel.js → channels/discover.js} +8 -12
  18. package/dist/channels/feishu/card.js +1 -1
  19. package/dist/channels/feishu/context-buffer.d.ts +1 -1
  20. package/dist/channels/feishu/context-buffer.js +4 -4
  21. package/dist/channels/feishu/feishu-api.d.ts +4 -2
  22. package/dist/channels/feishu/feishu-api.js +1 -1
  23. package/dist/channels/feishu/feishu.d.ts +1 -1
  24. package/dist/channels/feishu/feishu.js +45 -15
  25. package/dist/channels/feishu/invoke-turn.d.ts +9 -3
  26. package/dist/channels/feishu/invoke-turn.js +152 -34
  27. package/dist/channels/feishu/parse.js +6 -0
  28. package/dist/channels/feishu/preview.d.ts +1 -1
  29. package/dist/channels/feishu/preview.js +7 -3
  30. package/dist/channels/feishu/ws-ingress.d.ts +1 -1
  31. package/dist/channels/github/github.d.ts +1 -1
  32. package/dist/channels/http.d.ts +11 -7
  33. package/dist/channels/http.js +16 -111
  34. package/dist/channels/{context-buffer.js → kit/context-buffer.js} +2 -2
  35. package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +18 -4
  36. package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +23 -8
  37. package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
  38. package/dist/channels/{seen.js → kit/seen.js} +1 -1
  39. package/dist/channels/kit/state.d.ts +6 -0
  40. package/dist/channels/{state.js → kit/state.js} +4 -24
  41. package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
  42. package/dist/channels/{stop-command.js → kit/stop-command.js} +2 -2
  43. package/dist/channels/{tasks.js → kit/tasks.js} +1 -1
  44. package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +7 -0
  45. package/dist/channels/{thread-participants.js → kit/thread-participants.js} +4 -1
  46. package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
  47. package/dist/channels/{turn-store.js → kit/turn-store.js} +1 -1
  48. package/dist/channels/lark/lark.d.ts +1 -1
  49. package/dist/channels/serve.d.ts +73 -0
  50. package/dist/channels/serve.js +243 -0
  51. package/dist/channels/slack/bot-auth.js +3 -14
  52. package/dist/channels/slack/context-buffer.d.ts +2 -2
  53. package/dist/channels/slack/context-buffer.js +2 -2
  54. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  55. package/dist/channels/slack/invoke-turn.js +9 -6
  56. package/dist/channels/slack/parse.js +2 -2
  57. package/dist/channels/slack/preview.d.ts +1 -1
  58. package/dist/channels/slack/preview.js +11 -3
  59. package/dist/channels/slack/slack-api.js +1 -1
  60. package/dist/channels/slack/slack.d.ts +1 -1
  61. package/dist/channels/slack/slack.js +8 -12
  62. package/dist/channels/slack/welcomed.js +1 -1
  63. package/dist/channels/telegram/context-buffer.d.ts +2 -2
  64. package/dist/channels/telegram/context-buffer.js +2 -2
  65. package/dist/channels/telegram/invoke-turn.d.ts +1 -1
  66. package/dist/channels/telegram/invoke-turn.js +2 -2
  67. package/dist/channels/telegram/parse.js +2 -2
  68. package/dist/channels/telegram/preview.d.ts +1 -1
  69. package/dist/channels/telegram/preview.js +6 -2
  70. package/dist/channels/telegram/telegram.d.ts +1 -1
  71. package/dist/channels/telegram/telegram.js +3 -3
  72. package/dist/channels/telegram/turn-store.d.ts +2 -2
  73. package/dist/channels/telegram/turn-store.js +2 -2
  74. package/dist/cli/commands/add.js +1 -1
  75. package/dist/cli/commands/deploy.js +13 -5
  76. package/dist/cli/commands/dev.js +26 -14
  77. package/dist/cli/commands/fire.js +2 -2
  78. package/dist/cli/commands/info.js +28 -5
  79. package/dist/cli/commands/invoke.js +1 -1
  80. package/dist/cli/commands/schedule.js +1 -1
  81. package/dist/cli/commands/start.js +64 -95
  82. package/dist/cli/commands/tool.js +5 -7
  83. package/dist/cli/program.js +1 -1
  84. package/dist/cli/serve.d.ts +18 -57
  85. package/dist/cli/serve.js +57 -243
  86. package/dist/cli/shared.d.ts +5 -3
  87. package/dist/cli/shared.js +14 -7
  88. package/dist/collect.d.ts +30 -4
  89. package/dist/collect.js +39 -6
  90. package/dist/core.d.ts +3 -5
  91. package/dist/core.js +7 -5
  92. package/dist/deploy/preflight.d.ts +8 -2
  93. package/dist/deploy/preflight.js +22 -4
  94. package/dist/deploy/secrets.d.ts +3 -0
  95. package/dist/deploy/secrets.js +6 -0
  96. package/dist/dev-supervisor.js +16 -4
  97. package/dist/engines/pi/agent-session-factory.d.ts +104 -0
  98. package/dist/engines/pi/agent-session-factory.js +314 -0
  99. package/dist/engines/pi/config.d.ts +4 -4
  100. package/dist/engines/pi/config.js +1 -1
  101. package/dist/engines/pi/create.d.ts +56 -42
  102. package/dist/engines/pi/create.js +150 -75
  103. package/dist/engines/pi/definition.d.ts +41 -7
  104. package/dist/engines/pi/definition.js +115 -11
  105. package/dist/engines/pi/invoke-session.d.ts +35 -0
  106. package/dist/engines/pi/invoke-session.js +357 -0
  107. package/dist/engines/pi/login.js +7 -4
  108. package/dist/engines/pi/models.d.ts +59 -11
  109. package/dist/engines/pi/models.js +64 -9
  110. package/dist/engines/pi/open.d.ts +9 -6
  111. package/dist/engines/pi/open.js +20 -10
  112. package/dist/engines/pi/report.d.ts +0 -4
  113. package/dist/engines/pi/report.js +0 -7
  114. package/dist/engines/pi/service.d.ts +23 -0
  115. package/dist/engines/pi/service.js +19 -0
  116. package/dist/engines/pi/session-builder.js +88 -90
  117. package/dist/engines/pi/session-control.d.ts +11 -9
  118. package/dist/engines/pi/session-control.js +114 -77
  119. package/dist/engines/pi/session-inheritance.d.ts +44 -0
  120. package/dist/engines/pi/session-inheritance.js +242 -0
  121. package/dist/engines/pi/session-settings.d.ts +14 -2
  122. package/dist/engines/pi/session-settings.js +18 -0
  123. package/dist/engines/pi/session-store.d.ts +70 -0
  124. package/dist/engines/pi/session-store.js +313 -0
  125. package/dist/engines/pi/tool-context.d.ts +18 -8
  126. package/dist/engines/pi/tool-context.js +23 -1
  127. package/dist/engines/pi/tool.d.ts +5 -5
  128. package/dist/engines/pi/tool.js +1 -1
  129. package/dist/engines/pi/{invoke.d.ts → turn-kit.d.ts} +46 -45
  130. package/dist/engines/pi/turn-kit.js +188 -0
  131. package/dist/engines/pi/wake-tool.js +1 -1
  132. package/dist/index.d.ts +1 -0
  133. package/dist/index.js +1 -0
  134. package/dist/log.d.ts +3 -0
  135. package/dist/log.js +5 -1
  136. package/dist/node.d.ts +16 -0
  137. package/dist/node.js +20 -0
  138. package/dist/paths.d.ts +13 -0
  139. package/dist/paths.js +16 -0
  140. package/dist/pi.d.ts +15 -8
  141. package/dist/pi.js +7 -6
  142. package/dist/scaffold/templates/fastagent.config.mjs +2 -0
  143. package/dist/scaffold/templates/persona.md +2 -2
  144. package/dist/schedule/state.js +4 -9
  145. package/dist/schedule/wakeups.d.ts +1 -1
  146. package/dist/schedule/wakeups.js +1 -1
  147. package/dist/service.d.ts +158 -0
  148. package/dist/service.js +354 -0
  149. package/dist/session-remote.d.ts +1 -1
  150. package/dist/session-remote.js +10 -2
  151. package/dist/session.d.ts +4 -2
  152. package/dist/session.js +1 -1
  153. package/package.json +14 -5
  154. package/dist/channels/state.d.ts +0 -16
  155. package/dist/engines/pi/harness.d.ts +0 -112
  156. package/dist/engines/pi/harness.js +0 -153
  157. package/dist/engines/pi/invoke.js +0 -618
  158. package/dist/engines/pi/read-image.d.ts +0 -4
  159. package/dist/engines/pi/read-image.js +0 -62
  160. package/dist/engines/pi/sessions.d.ts +0 -44
  161. package/dist/engines/pi/sessions.js +0 -182
  162. package/dist/host/node.js +0 -52
  163. /package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +0 -0
  164. /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
  165. /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
  166. /package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +0 -0
  167. /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
  168. /package/dist/channels/{text.js → kit/text.js} +0 -0
  169. /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
  170. /package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +0 -0
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * The pi `Models` collection — the single hub that owns BOTH model resolution (provider/modelId
3
3
  * lookup) AND auth (per-request credential resolution). fastagent builds one per opener and threads
4
- * it into the harness alongside the selected `model`; the two must come from the same collection so
4
+ * it into the engine alongside the selected `model`; the two must come from the same collection so
5
5
  * the model's provider auth is in scope.
6
6
  */
7
+ import { join } from "node:path";
7
8
  import { defaultProviderAuthContext } from "@earendil-works/pi-ai";
8
9
  import { builtinModels } from "@earendil-works/pi-ai/providers/all";
9
10
  import { ModelRuntime } from "@earendil-works/pi-coding-agent";
10
11
  import { fastagentCredentialStore } from "./auth.js";
12
+ import { providerOf } from "./config.js";
11
13
  import { interactiveLoginKind } from "./login.js";
14
+ import { AGENT_MODELS_FILE, resolveStateRoot } from "../../paths.js";
12
15
  /**
13
16
  * A `Models` with every built-in pi provider, wired to fastagent's auth: stored credentials from the
14
17
  * {@link CreatePiModelsOptions.authPath} file (via {@link fastagentCredentialStore}; the global
@@ -25,21 +28,73 @@ export function createPiModels(options = {}) {
25
28
  models.setProvider(provider);
26
29
  return models;
27
30
  }
31
+ /**
32
+ * The serving default for reasoning effort, pinned to what pi's TUI defaults to (its own
33
+ * DEFAULT_THINKING_LEVEL) — NOT inherited from the engine, whose fallback is "off": an author vibes
34
+ * at "medium" in pi and must get "medium" when served (fidelity), and pinning the value here means
35
+ * an upstream default change in either place cannot silently alter deployments. Models that do not
36
+ * support a level are clamped by pi per model.
37
+ */
38
+ export const DEFAULT_THINKING_LEVEL = "medium";
28
39
  /**
29
40
  * The `ModelRuntime`-shaped sibling of {@link createPiModels} — the SAME hub semantics (built-in
30
41
  * providers + fastagent's credential store at `authPath`) in the type pi's session services require
31
- * (`createAgentSessionServices({ modelRuntime })`). Builtins only (`modelsPath: null` — pi's
32
- * machine-global models.json is definition-foreign) and no availability network, so the model
33
- * surface equals serving's. No `providers` option: `ModelRuntime` registers providers by config
34
- * record, not `Provider` instance — accepting the option and dropping it would be a silent no-op;
35
- * add the mapping when a consumer actually needs it.
42
+ * (`createAgentSessionServices({ modelRuntime })`). Built-ins PLUS the agent's own
43
+ * {@link AGENT_MODELS_FILE} when `agentDir` is given (a dir-less caller gets built-ins only), and no
44
+ * availability network, so the model surface equals serving's.
45
+ *
46
+ * `ModelRuntime` also takes `Provider` INSTANCES via `registerNativeProvider` (pi 0.83); the
47
+ * declarative file is what this rung wires because it is data that travels with the definition.
36
48
  */
37
- export function createPiModelRuntime(options = {}) {
38
- return ModelRuntime.create({
49
+ export async function createPiModelRuntime(options = {}) {
50
+ const { agentDir } = options;
51
+ const runtime = await ModelRuntime.create({
39
52
  credentials: fastagentCredentialStore(options.authPath, { warn: options.warn }),
40
- modelsPath: null,
53
+ modelsPath: agentDir ? join(agentDir, AGENT_MODELS_FILE) : null,
54
+ // MUST be set whenever modelsPath is: pi defaults this to `<dirname(modelsPath)>/models-store.json`,
55
+ // which would write a generated cache INTO the author's agent dir — and `deploy` bakes the whole
56
+ // tree, so it would travel into the image as stale state. Machinery belongs under the state root.
57
+ ...(agentDir
58
+ ? { modelsStorePath: join(options.stateRoot ?? resolveStateRoot(agentDir), "models-store.json") }
59
+ : {}),
41
60
  allowModelNetwork: false,
42
61
  });
62
+ // A malformed models.json does NOT throw upstream — `create` resolves with the built-ins and parks the
63
+ // reason in getError(). Left unread, a typo'd endpoint would silently degrade to "provider not in
64
+ // registry" at model-resolution time, i.e. the silent fallback this codebase forbids. The upstream
65
+ // message already names both the reason and the file, so it is surfaced verbatim.
66
+ const error = runtime.getError();
67
+ if (error)
68
+ throw new Error(error);
69
+ for (const provider of options.providers ?? [])
70
+ runtime.registerNativeProvider(provider);
71
+ return runtime;
72
+ }
73
+ /**
74
+ * How a model's credential will REACH a deployed agent — the question `deploy` asks, which
75
+ * {@link probeAuthSource} cannot answer: it flattens every models.json endpoint to the display label
76
+ * "configured API key", so a self-hosted endpoint looks credential-less to the deploy gate even when
77
+ * its key is sitting in an env var.
78
+ *
79
+ * - `envVar`: an environment variable backs it, BY NAME — the shape `deploy` already understands, so
80
+ * the value carries as a host secret with no extra declaration from the author.
81
+ * - `inDefinition`: the definition itself carries it (a literal `apiKey`, or a `!command` run on the
82
+ * host). Nothing for `deploy` to carry — and nothing to gate on either, which is the point: the
83
+ * `fastagent login` remedy is meaningless for a provider login cannot serve.
84
+ *
85
+ * Neither set = a stored credential or nothing at all; the existing auth.json / gate paths decide.
86
+ */
87
+ export function modelCredentialCarry(runtime, spec) {
88
+ const status = runtime.getProviderAuthStatus(providerOf(spec));
89
+ if (!status.configured)
90
+ return { inDefinition: false };
91
+ // An env-var name is only useful downstream if it IS one: `"${A}_${B}"` interpolation resolves from
92
+ // the environment but has no single name to carry, so it falls through to the definition-carried
93
+ // branch, where the author's `deploy.secrets` is the mechanism.
94
+ if (status.source === "environment" && status.label && /^[A-Z][A-Z0-9_]*$/.test(status.label)) {
95
+ return { envVar: status.label, inDefinition: false };
96
+ }
97
+ return { inDefinition: status.source !== "stored" };
43
98
  }
44
99
  /**
45
100
  * Probe every provider's auth once (auth is provider-scoped, so any of its models works as the probe)
@@ -1,10 +1,10 @@
1
1
  import type { Agent } from "../../agent.ts";
2
2
  import { type FastagentConfig } from "./config.ts";
3
3
  import type { SessionControl } from "../../session.ts";
4
- import type { SessionObserver } from "./invoke.ts";
5
- import type { PiSessionReader, PiSessionStore } from "./sessions.ts";
4
+ import type { SessionObserver } from "./turn-kit.ts";
6
5
  import type { ModuleLoadFailure } from "../../loader.ts";
7
6
  import { type LoadedDefinition } from "./definition.ts";
7
+ import { type PiSessionRecordStore } from "./session-store.ts";
8
8
  import type { ToolCollision } from "./tool.ts";
9
9
  import type { MountedTool } from "./tool.ts";
10
10
  export interface CreatePiAgentFromDirOptions {
@@ -44,7 +44,7 @@ export interface CreatePiAgentFromDirOptions {
44
44
  }
45
45
  /**
46
46
  * The agent assembly FRONT HALF — everything that is independent of how pi consumes the
47
- * definition (transient harness for serving vs resident AgentSession for chat / session control):
47
+ * definition (a per-invoke session for serving vs a resident one for chat):
48
48
  * placement resolution → config → model spec → the full tool surface ({@link resolveAgentTools} — the
49
49
  * ONE place it is computed) → state root → auth path. Both {@link createPiAgentFromDir} and the
50
50
  * session builder (session-builder.ts) consume this, so THESE inputs cannot drift between the two
@@ -65,7 +65,7 @@ export interface AgentAssembly {
65
65
  stateRoot: string;
66
66
  /** Absolute credentials file (--auth-path/authPath option > FASTAGENT_AUTH_PATH > <agentDir>/.secrets/auth.json). */
67
67
  authPath: string;
68
- /** The full mounted tool surface (config.tools + discovered tools/, search_tools applied). */
68
+ /** The full mounted tool surface (all coding tools + config.tools + discovered tools/, search_tools applied). */
69
69
  tools: MountedTool[];
70
70
  toolNames: string[];
71
71
  deferredToolNames: string[];
@@ -99,10 +99,13 @@ export declare function createPiAgentFromDir(dir: string, options?: CreatePiAgen
99
99
  sessionsDir: string;
100
100
  /** Absolute credentials file in use (for the startup report). */
101
101
  authPath: string;
102
- /** The session store in use — also a {@link PiSessionReader}. */
103
- sessions: PiSessionStore & PiSessionReader;
102
+ /** The session store in use. */
103
+ sessions: PiSessionRecordStore;
104
104
  /** The observation plane over this agent's sessions; present iff `options.sessionControl`. */
105
105
  sessionControl?: SessionControl;
106
+ /** Whether the agent schedules its own follow-up turns — read from the config, so a caller
107
+ * assembling a service does not have to reach back into it (MountableAgent). */
108
+ selfSchedule: boolean;
106
109
  /** Non-default, active-by-default tool names in effect: config.tools + discovered tools/. Each name
107
110
  * lives in exactly one report slot — deferred names are in {@link deferredToolNames} instead. */
108
111
  toolNames: string[];
@@ -16,7 +16,7 @@ import { createPiSessionControl } from "./session-control.js";
16
16
  import { withWakeTool } from "./wake-tool.js";
17
17
  import { loadAgentSkills } from "./definition.js";
18
18
  import { reportFindingsIfChanged } from "./report.js";
19
- import { jsonlSessionStore } from "./sessions.js";
19
+ import { piSessionRecordStore } from "./session-store.js";
20
20
  export async function resolveAgentAssembly(dir, options = {}) {
21
21
  // Placement is structural (resolvePlacement): the AGENT DIR carries definition + config + machinery;
22
22
  // its parent — the WORKSPACE — is what the agent works on: its cwd and the start of the ②-context
@@ -27,7 +27,7 @@ export async function resolveAgentAssembly(dir, options = {}) {
27
27
  if (!modelSpec) {
28
28
  throw new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
29
29
  }
30
- const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir);
30
+ const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir, workspace);
31
31
  // The state root: sessions/channel state/schedule state derive from it (FASTAGENT_STATE_DIR moves it
32
32
  // in one knob — a container points it at its volume); the finer overrides below still win.
33
33
  const stateRoot = resolveStateRoot(agentDir);
@@ -62,13 +62,14 @@ export async function createPiAgentFromDir(dir, options = {}) {
62
62
  const mountedTools = withWakeTool(tools, stateRoot, !!options.serving && !!config.selfSchedule);
63
63
  const sessionsDir = options.sessionsDir ?? defaultSessionsDir(stateRoot);
64
64
  await mkdir(sessionsDir, { recursive: true });
65
- const sessions = jsonlSessionStore({ dir: sessionsDir, cwd: workspace });
65
+ const sessions = piSessionRecordStore({ dir: sessionsDir, cwd: workspace });
66
66
  // The hub is wired HERE because the store is created here: chicken-and-egg otherwise (the hub
67
- // needs the store; the agent needs the hub's observer). Boundary parts (models/factory/lease)
67
+ // needs the store; the agent needs the hub's observer). Boundary parts (factory/lease/registry)
68
68
  // only exist after the assembly below — the hub takes them as a lazy thunk, filled by the
69
69
  // assembly's onAssembly callback (assembly completes before this function returns, so every
70
70
  // dispatch sees them). An extra caller observer composes after the hub's (TRUSTED seam).
71
71
  let boundaryParts;
72
+ let assembled;
72
73
  const caller = options.observer;
73
74
  const wantControl = options.sessionControl ?? (config.sessionControl === true && options.serving === true);
74
75
  const hub = wantControl
@@ -117,20 +118,29 @@ export async function createPiAgentFromDir(dir, options = {}) {
117
118
  observer,
118
119
  onAssembly: hub
119
120
  ? (parts) => {
120
- boundaryParts = {
121
- lease: parts.lease,
122
- models: parts.models,
123
- harnessFactory: parts.harnessFactory,
124
- defaults: parts.defaults,
125
- };
121
+ assembled = parts;
126
122
  }
127
123
  : undefined,
128
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
+ }
129
138
  return {
130
139
  agent,
131
140
  definition,
132
141
  sessions,
133
142
  sessionControl: hub?.control,
143
+ selfSchedule: config.selfSchedule ?? false,
134
144
  agentDir,
135
145
  workspace,
136
146
  config,
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import type { SkillDiagnostic } from "@earendil-works/pi-agent-core";
6
6
  import type { SkillCollision } from "./definition.ts";
7
- import type { ModuleLoadFailure } from "../../loader.ts";
8
7
  import type { ToolCollision } from "./tool.ts";
9
8
  type Findings = {
10
9
  collisions: SkillCollision[];
@@ -23,7 +22,4 @@ type Findings = {
23
22
  export declare function reportFindingsIfChanged(dir: string, def: Findings): void;
24
23
  export declare function reportDefinitionWarnings(collisions: SkillCollision[], diagnostics: SkillDiagnostic[]): void;
25
24
  export declare function reportToolCollisions(collisions: ToolCollision[]): void;
26
- /** Report per-file module failures. The caller decides whether they are degradations (tools/schedules)
27
- * or fatal (declared channels on the serving path). */
28
- export declare function reportModuleLoadFailures(failures: ModuleLoadFailure[]): void;
29
25
  export {};
@@ -42,10 +42,3 @@ export function reportToolCollisions(collisions) {
42
42
  log.warn(`[fastagent] tool "${c.name}" (${c.source}) dropped — a default/config tool already uses that name`);
43
43
  }
44
44
  }
45
- /** Report per-file module failures. The caller decides whether they are degradations (tools/schedules)
46
- * or fatal (declared channels on the serving path). */
47
- export function reportModuleLoadFailures(failures) {
48
- for (const f of failures) {
49
- log.warn(`[fastagent] ${f.label} failed to load, skipping it — ${f.message}`);
50
- }
51
- }
@@ -0,0 +1,23 @@
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
+ */
9
+ import type { AgentService, MountAgentServiceOptions } from "../../service.ts";
10
+ export interface CreateAgentServiceOptions extends MountAgentServiceOptions {
11
+ model?: string;
12
+ authPath?: string;
13
+ sessionsDir?: string;
14
+ }
15
+ /**
16
+ * Open an agent directory as a live service: one handler, mounted wherever you serve.
17
+ *
18
+ * ```ts
19
+ * const service = await createAgentService("./my-agent");
20
+ * app.use("/agent", nodeListener(service.handler));
21
+ * ```
22
+ */
23
+ export declare function createAgentService(dir: string, options?: CreateAgentServiceOptions): Promise<AgentService>;
@@ -0,0 +1,19 @@
1
+ import { mountAgentService } from "../../service.js";
2
+ import { createPiAgentFromDir } from "./open.js";
3
+ /**
4
+ * Open an agent directory as a live service: one handler, mounted wherever you serve.
5
+ *
6
+ * ```ts
7
+ * const service = await createAgentService("./my-agent");
8
+ * app.use("/agent", nodeListener(service.handler));
9
+ * ```
10
+ */
11
+ export async function createAgentService(dir, options = {}) {
12
+ const opened = await createPiAgentFromDir(dir, {
13
+ ...(options.model !== undefined ? { model: options.model } : {}),
14
+ ...(options.authPath !== undefined ? { authPath: options.authPath } : {}),
15
+ ...(options.sessionsDir !== undefined ? { sessionsDir: options.sessionsDir } : {}),
16
+ serving: true, // a mounted service is long-running: the scheduler poller runs
17
+ });
18
+ return mountAgentService(opened, options);
19
+ }
@@ -11,8 +11,8 @@
11
11
  * - prompt → systemPromptOverride = base + instructions ONLY; pi appends the skill section and env
12
12
  * (cwd) itself (including it here would duplicate it).
13
13
  * - skills → skillsOverride (fastagent's skills, for the section + invocation).
14
- * - tools → default coding tools by NAME (pi rebuilds them cwd-bound for rich rendering) +
15
- * fastagent's custom tools via pi's customTools path (so they survive /new, /resume, fork).
14
+ * - tools → all seven coding tools plus authored tools through pi's customTools path; pi's builtin
15
+ * copies are suppressed, and the injected set survives /new, /resume, and fork.
16
16
  * - models → a ModelRuntime with builtins only (`modelsPath: null`, no availability network), so
17
17
  * the model surface equals serving's `createPiModels()` — pi's machine-global
18
18
  * models.json does not leak in.
@@ -28,32 +28,19 @@
28
28
  * workspace.
29
29
  */
30
30
  import { existsSync, readFileSync } from "node:fs";
31
- import { dirname, join, resolve } from "node:path";
31
+ import { join, resolve } from "node:path";
32
32
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
33
33
  import { SessionManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, } from "@earendil-works/pi-coding-agent";
34
+ import { definitionResourceLoaderOptions, reportExtensionErrors } from "./agent-session-factory.js";
34
35
  import { resolveModel } from "./config.js";
35
- import { assembleSystemPrompt, piBasePrompt, piDefaultTools } from "./create.js";
36
- import { canonicalPath, loadAgentDefinition } from "./definition.js";
36
+ import { assembleSystemPrompt, piBasePrompt } from "./create.js";
37
+ import { canonicalPath, loadAgentDefinition, loadExtensionPaths } from "./definition.js";
37
38
  import { createPiModelRuntime, probeAuthSource } from "./models.js";
38
39
  import { log } from "../../log.js";
39
- import { additiveActivation, turnContext } from "./tool-context.js";
40
- import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "./report.js";
40
+ import { additiveActivation, agentSessionManager, turnContext, } from "./tool-context.js";
41
+ import { reportFindingsIfChanged, reportToolCollisions } from "./report.js";
42
+ import { reportModuleLoadFailures } from "../../log.js";
41
43
  import { resolveAgentAssembly } from "./open.js";
42
- /** Adapt coding-agent's resident SessionManager to FastAgent's shared tool-runtime manager port. */
43
- function toolChatSessionManager(session) {
44
- return {
45
- getSessionId: () => session.sessionManager.getSessionId(),
46
- async getHeader() {
47
- const header = session.sessionManager.getHeader();
48
- if (!header)
49
- throw new Error("chat session has no metadata header");
50
- return { id: header.id, timestamp: header.timestamp };
51
- },
52
- async getBranch() {
53
- return session.sessionManager.getBranch();
54
- },
55
- };
56
- }
57
44
  /**
58
45
  * Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
59
46
  * skills, and auth resolved exactly as the serving opener does). Split from the TUI launcher so the
@@ -63,7 +50,7 @@ export async function buildAgentSessionRuntime(dir, options = {},
63
50
  /** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
64
51
  sessionManager) {
65
52
  /** The turn's {@link ToolActivation} over pi's AgentSession — the counterpart of invoke.ts's
66
- * harness bridge, so the SAME builtin search_tools serves both paths. Additive; unknown names
53
+ * serving bridge, so the SAME builtin search_tools serves both paths. Additive; unknown names
67
54
  * filtered (`setActiveToolsByName` is authoritative on the session and rebuilds its prompt — our
68
55
  * static override keeps the prompt identical to serving). */
69
56
  function sessionToolActivation(session) {
@@ -99,34 +86,27 @@ sessionManager) {
99
86
  // (what you iterate is what you serve): the initial active set excludes deferred tools (applied
100
87
  // on the session in createRuntime — pi's session starts all-active), and the activation bridge
101
88
  // above rides the same turn context, so the SAME search_tools works against pi's AgentSession
102
- // instead of fastagent's harness.
103
- const { config, modelSpec, agentDir, authPath, tools, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
89
+ // instead of the served one.
90
+ const { config, modelSpec, agentDir, authPath, stateRoot, tools, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
104
91
  reportToolCollisions(toolCollisions);
105
92
  reportModuleLoadFailures(toolFailures);
106
- // ONE hub owns model resolution AND per-request auth the ModelRuntime-shaped sibling of
107
- // serving's createPiModels; see models.ts.
108
- const modelRuntime = await createPiModelRuntime({ authPath });
109
- // MIGRATION HINT (deliberate breaking change): chat historically used pi's own `~/.pi` auth;
110
- // it now reads the agent's credential file like every other command. Probe the RESOLVED
111
- // model's provider through the normal resolution path (stored credential OR env var — an
112
- // env-authed user is fine and must not be warned): only when that provider has no usable auth
113
- // AND pi's old file exists does the bare provider error get its cause named.
114
- if ((await probeAuthSource(modelRuntime, modelSpec)) === undefined &&
115
- existsSync(join(getAgentDir(), "auth.json"))) {
116
- log.warn(`[fastagent] no credentials for ${modelSpec} in ${authPath} — this runtime no longer reads ` +
117
- `pi's ~/.pi auth; run \`fastagent login\` (or /login in the TUI) to store credentials for this agent`);
118
- }
119
- const model = resolveModel(modelRuntime, modelSpec);
93
+ // ONE hub owns model resolution AND per-request auth; see models.ts.
94
+ // agentDir carries the agent's own models.json (custom endpoints); stateRoot keeps pi's generated
95
+ // catalog cache out of the definition dir. See createPiModelRuntime.
96
+ const modelRuntime = await createPiModelRuntime({ authPath, agentDir, stateRoot });
120
97
  const env = new NodeExecutionEnv({ cwd });
121
98
  const definition = await loadAgentDefinition(agentDir, { cwd, env });
122
99
  reportFindingsIfChanged(definition.dir, definition);
123
- const defaultNames = piDefaultTools().map((t) => t.name);
124
- const customTools = tools.filter((t) => !defaultNames.includes(t.name));
100
+ // Assembly-time, like serving's: this whole function is memoized, so the scan and its warnings
101
+ // happen once per runtime rather than per session rebuild (/new, /resume, fork).
102
+ 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.
125
105
  // Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
126
106
  // it). Each execute runs inside the turn context with the CURRENT session's activation bridge — the
127
107
  // assembly is memoized across /new//resume/fork rebuilds while the session changes, so the bridge
128
- // resolves through sessionRef at call time, exactly like the serving path resolves its harness.
129
- const customToolDefs = customTools.map((t) => ({
108
+ // resolves through sessionRef at call time, exactly like the serving path resolves its session.
109
+ const customToolDefs = tools.map((t) => ({
130
110
  name: t.name,
131
111
  label: t.name,
132
112
  description: t.description ?? "",
@@ -142,10 +122,8 @@ sessionManager) {
142
122
  if (!bound)
143
123
  throw new Error("tool executed before its session was built (lifecycle invariant broken)");
144
124
  return turnContext.run({ cwd, sessionManager: bound.sessionManager, tools: bound.activation },
145
- // pi's per-turn TOOL context (5th parameter) is read only by its default coding tools, and
146
- // those are filtered out of `customTools` above; fastagent's own take theirs from
147
- // `turnContext` (AsyncLocalStorage). So this env exists to satisfy the shape, and it is the
148
- // chat cwd's — the same root a default tool would have got, had one reached here.
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.
149
127
  () => t.execute(id, params, signal, undefined, { env }));
150
128
  },
151
129
  }));
@@ -156,13 +134,14 @@ sessionManager) {
156
134
  contextFiles: definition.contextFiles,
157
135
  });
158
136
  return {
159
- model,
160
137
  modelRuntime,
138
+ modelSpec,
139
+ authPath,
161
140
  // Serving honors config.thinkingLevel (config → L2); the resident session must too (fidelity).
162
141
  thinkingLevel: config.thinkingLevel,
163
142
  definition,
164
- defaultNames,
165
- customTools,
143
+ extensionPaths,
144
+ customTools: tools,
166
145
  customToolDefs,
167
146
  deferredToolNames,
168
147
  systemPrompt,
@@ -192,65 +171,84 @@ sessionManager) {
192
171
  assembly ??= resolveAssembly(rootCwd);
193
172
  return assembly;
194
173
  };
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;
195
178
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
196
- const { model, modelRuntime, thinkingLevel, definition, defaultNames, customTools, customToolDefs, deferredToolNames, systemPrompt, } = await assemblyFor(cwd);
179
+ const { modelRuntime, modelSpec, authPath, thinkingLevel, definition, extensionPaths, customToolDefs, deferredToolNames, systemPrompt, } = await assemblyFor(cwd);
180
+ // Per session, NOT memoized with the assembly: pi replaces the session on /new, /resume and
181
+ // fork, and its extension contract is that the replacement gets freshly loaded extensions
182
+ // rather than the previous session's objects. The expensive halves (model hub, auth) are shared
183
+ // through the assembly; only the resource loader is rebuilt.
197
184
  const services = await createAgentSessionServices({
198
185
  cwd,
199
186
  // fastagent's models + auth hub replaces pi's default (~/.pi-backed) one — the auth
200
187
  // unification point; see the header.
201
188
  modelRuntime,
202
- resourceLoaderOptions: {
203
- // Definition-only, like dev/start: suppress pi's machine-global discovery (the developer's own
204
- // ~/.pi extensions, slash commands, global AGENTS.md, APPEND_SYSTEM.md) so this runtime runs
205
- // the same agent that gets served, not the authoring machine's pi setup on top.
206
- noExtensions: true,
207
- noPromptTemplates: true,
208
- noContextFiles: true,
209
- systemPromptOverride: () => systemPrompt,
210
- appendSystemPromptOverride: () => [],
211
- // Replace pi's discovered skills with fastagent's. fastagent's Skill (content inline) is
212
- // reshaped to pi-coding-agent's (read from filePath/baseDir at invocation time).
213
- skillsOverride: (base) => ({
214
- skills: definition.skills.map((s) => ({
215
- name: s.name,
216
- description: s.description,
217
- filePath: s.filePath,
218
- baseDir: dirname(s.filePath),
219
- sourceInfo: {
220
- path: s.filePath,
221
- source: "fastagent",
222
- scope: "project",
223
- origin: "top-level",
224
- baseDir: dirname(s.filePath),
225
- },
226
- disableModelInvocation: s.disableModelInvocation ?? false,
227
- })),
228
- diagnostics: base.diagnostics,
229
- }),
230
- },
189
+ // Chat's assembly is fixed for the life of the runtime (a rebuild makes a new one), so these
190
+ // read constants where serving passes accessors that change per turn — and it DOES pass its
191
+ // extension paths: chat runs them.
192
+ resourceLoaderOptions: definitionResourceLoaderOptions({
193
+ systemPrompt: () => systemPrompt,
194
+ skills: () => definition.skills,
195
+ extensionPaths,
196
+ }),
231
197
  });
198
+ reportExtensionErrors(services);
199
+ // AFTER the services, because an extension may be what defines the model. `registerProvider()`
200
+ // is pi's documented way for one to add providers, and extensions do not execute until the
201
+ // 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
+ }
217
+ const model = resolveModel(modelRuntime, modelSpec);
232
218
  const result = await createAgentSessionFromServices({
233
219
  services,
234
220
  sessionManager,
235
221
  sessionStartEvent,
236
222
  model,
237
223
  thinkingLevel,
238
- tools: [...defaultNames, ...customTools.map((t) => t.name)],
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",
239
231
  customTools: customToolDefs,
240
232
  });
241
233
  sessionRef.current = {
242
234
  session: result.session,
243
- sessionManager: toolChatSessionManager(result.session),
235
+ sessionManager: agentSessionManager(result.session, result.session.sessionManager.getSessionId()),
244
236
  activation: sessionToolActivation(result.session),
245
237
  };
246
- // Deferral emulation: pi's session starts with everything active — narrow it by SUBTRACTING
247
- // the deferred names from whatever is active (robust to pi mounting tools of its own; an
248
- // exact-set-equality gate would silently stop narrowing the day pi adds one). Applied on EVERY
249
- // build including /resume: pi's chat session does not record activations (its SessionContext has
250
- // no activeToolNames), so "restore prior activations" is not implementable here deferral stays
251
- // consistently ON and a resumed conversation re-discovers via search_tools (documented divergence
252
- // from serving, where activations persist in the session). The deferred SET comes from the shared
253
- // assembly (one definition of "deferred"), never recomputed here.
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.
254
252
  if (deferredToolNames.length > 0) {
255
253
  const active = result.session.getActiveToolNames();
256
254
  if (deferredToolNames.some((n) => active.includes(n))) {
@@ -1,9 +1,11 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import type { Models } from "@earendil-works/pi-ai";
3
3
  import { type AgentCommand, type SessionControl, type SessionEvent } from "../../session.ts";
4
- import type { Lease, SessionObserver } from "./invoke.ts";
5
- import { type AnyModel, type PiHarnessFactory } from "./harness.ts";
6
- import { type PiSessionReader } from "./sessions.ts";
4
+ import type { SessionObserver } from "./turn-kit.ts";
5
+ import type { Lease } from "./turn-kit.ts";
6
+ import type { AnyModel } from "./models.ts";
7
+ import type { PiAgentSessionFactory } from "./invoke-session.ts";
8
+ import type { PiSessionRecordStore } from "./session-store.ts";
7
9
  /** Ceiling for one subscriber's unconsumed backlog. A consumer this far behind (a stalled remote
8
10
  * connection — the wire's ReadableStream backpressure stops pulling while invokes keep pushing)
9
11
  * has its buffer FROZEN at the cap (memory bounded — the actual goal: ≈10k small events ≈ a few
@@ -15,25 +17,25 @@ import { type PiSessionReader } from "./sessions.ts";
15
17
  export declare const SUBSCRIBER_BUFFER_CAP = 10000;
16
18
  /** What boundary mutations (compact / set_model / set_thinking / navigate) need — the SAME instances the
17
19
  * agent assembly uses: the lease (mutations must not race a run), the model registry (validation +
18
- * allowedModels), and the harness factory (compaction is a model call). Writes go through the
19
- * session the hub's reader opened — after an existence check, so the control plane never creates
20
+ * allowedModels), and the session factory (compaction is a model call). Writes go through the
21
+ * record the hub's reader opened — after an existence check, so the control plane never creates
20
22
  * a session (that is the data plane's monopoly). */
21
23
  export interface PiBoundaryWiring {
22
24
  lease: Lease;
23
25
  models: Models;
24
- harnessFactory: PiHarnessFactory;
26
+ sessionFactory: PiAgentSessionFactory;
25
27
  /** The assembly's configured PAIR — what a session with no overrides runs on. One field because
26
28
  * model and thinking level are one setting: which levels exist is a property of the model, so a
27
29
  * wiring that could carry them apart could carry a pair no run uses. Must be what
28
- * {@link harnessFactory} was built with. */
30
+ * {@link sessionFactory} was built with. */
29
31
  defaults: {
30
32
  model: AnyModel;
31
33
  thinkingLevel: ThinkingLevel;
32
34
  };
33
35
  }
34
36
  export interface CreatePiSessionControlOptions {
35
- /** Read-only access to the durable session repository (the same root the agent writes). */
36
- sessions: PiSessionReader;
37
+ /** Read-only access to the durable session records (the same root the agent writes). */
38
+ sessions: PiSessionRecordStore;
37
39
  /** Boundary-mutation wiring, as a LAZY thunk: the hub's observer must exist before the agent
38
40
  * assembly that produces these parts, so the hub asks for them at dispatch time instead
39
41
  * (assembly completes before any dispatch can arrive). Absent / undefined → boundary commands