@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
@@ -4,14 +4,14 @@ import { isAbsolute, join } from "node:path";
4
4
  import { log } from "../../log.js";
5
5
  import { readBodyCapped } from "../body.js";
6
6
  import { text } from "../respond.js";
7
- import { createSeenRing } from "../seen.js";
8
- import { createThreadParticipants } from "../thread-participants.js";
9
- import { createTaskTracker } from "../tasks.js";
10
- import { ensureStateHome, removeRetiredStateFile } from "../state.js";
11
- import { dispatchStop, isStopText } from "../stop-command.js";
12
- import { codePointPrefix } from "../text.js";
13
- import { createTurnQueue } from "../turn-queue.js";
14
- import { createTurnStore } from "../turn-store.js";
7
+ import { createSeenRing } from "../kit/seen.js";
8
+ import { createThreadParticipants } from "../kit/thread-participants.js";
9
+ import { createTaskTracker } from "../kit/tasks.js";
10
+ import { ensureStateHome } from "../kit/state.js";
11
+ import { dispatchStop, isStopText } from "../kit/stop-command.js";
12
+ import { codePointPrefix } from "../kit/text.js";
13
+ import { createTurnQueue } from "../kit/turn-queue.js";
14
+ import { createTurnStore } from "../kit/turn-store.js";
15
15
  import { createSlackBotTokenProvider } from "./bot-auth.js";
16
16
  import { collectSlackBufferedFiles, createSlackContextBuffer } from "./context-buffer.js";
17
17
  import { invokeSlackTurn } from "./invoke-turn.js";
@@ -141,10 +141,6 @@ export function slackChannel(options) {
141
141
  * under a custom route is the `routed.session === undefined` condition on the write, not the
142
142
  * absence of a route: a route that supplies its own session records nothing here. */
143
143
  const threadKey = (teamId, channelId, threadTs) => `slack:${teamId}:${channelId}:${threadTs}`;
144
- // The participant model replaced the owned-thread index (a cache, so nothing is lost). REMOVE THIS
145
- // after the release following the participant model ships — by then no live deployment can still
146
- // be carrying the file. test/migration-deadline.test.ts fails when due.
147
- removeRetiredStateFile(stateHome, "owned-threads.json", label);
148
144
  const welcomed = createWelcomedUsers(join(stateHome, "welcomed.json"), label);
149
145
  const buffer = createSlackContextBuffer(join(stateHome, "buffers.json"), label);
150
146
  const store = createTurnStore(join(stateHome, "turns.json"), {
@@ -1,6 +1,6 @@
1
1
  /** Durable set of users who have already received the first-run DM welcome, so it is sent at most once. */
2
2
  import { log } from "../../log.js";
3
- import { loadStateFile, saveStateFile } from "../state.js";
3
+ import { loadStateFile, saveStateFile } from "../kit/state.js";
4
4
  function key(teamId, userId) {
5
5
  return `${teamId}:${userId}`;
6
6
  }
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Telegram's half of the shared context buffer (mechanics + consume protocol: ../context-buffer.ts):
2
+ * Telegram's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts):
3
3
  * the entry shape, its fold-line rendering, and buffered-attachment selection. Bucketed by Telegram
4
4
  * "place" (chat[:thread]), not session: an un-summoned message has no route session, and the flush
5
5
  * feeds whatever turn answers that place.
6
6
  */
7
- import { type ContextBuffer as GenericContextBuffer } from "../context-buffer.ts";
7
+ import { type ContextBuffer as GenericContextBuffer } from "../kit/context-buffer.ts";
8
8
  /** One buffered un-summoned message (object identity is the commit key). Besides the sender label and
9
9
  * one-line body, it carries what a LATER summon needs to resolve references into the discussion:
10
10
  * message ids ("reply to the one Alex answered"), and attachment file_ids so "summarize the file from
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Telegram's half of the shared context buffer (mechanics + consume protocol: ../context-buffer.ts):
2
+ * Telegram's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts):
3
3
  * the entry shape, its fold-line rendering, and buffered-attachment selection. Bucketed by Telegram
4
4
  * "place" (chat[:thread]), not session: an un-summoned message has no route session, and the flush
5
5
  * feeds whatever turn answers that place.
6
6
  */
7
- import { BUFFER_ATTACH_MAX, createContextBuffer as createGenericContextBuffer, } from "../context-buffer.js";
7
+ import { BUFFER_ATTACH_MAX, createContextBuffer as createGenericContextBuffer, } from "../kit/context-buffer.js";
8
8
  function bufferLine(e) {
9
9
  const meta = [
10
10
  e.messageId !== undefined ? `msg ${e.messageId}` : undefined,
@@ -6,7 +6,7 @@
6
6
  * wiring and the per-turn lifecycle.
7
7
  */
8
8
  import type { Agent, AgentEvent } from "../../agent.ts";
9
- import { type BusyRetry } from "../invoke-turn-kit.ts";
9
+ import { type BusyRetry } from "../kit/invoke-turn-kit.ts";
10
10
  import type { BufferedRef } from "./context-buffer.ts";
11
11
  /** Everything the transport needs to fetch a turn's attachments. */
12
12
  export interface TurnTransport {
@@ -1,5 +1,5 @@
1
1
  import { log } from "../../log.js";
2
- import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, missingAttachmentsNote, streamTurnWithBusyRetry, } from "../invoke-turn-kit.js";
2
+ import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, missingAttachmentsNote, streamTurnWithBusyRetry, } from "../kit/invoke-turn-kit.js";
3
3
  import { resolveFiles, resolveImages } from "./telegram-api.js";
4
4
  /** Appended to the prompt (not the system prompt): the channel owns Telegram-HTML formatting. */
5
5
  const HTML_INSTRUCTION = "\n\n(Format your reply in Telegram-supported HTML — <b> <i> <u> <s> <code> <pre> <a href> — not Markdown.)";
@@ -76,5 +76,5 @@ export async function* invokeTurn(agent, session, text, transport, attachments,
76
76
  return;
77
77
  }
78
78
  const prompt = { text: `${text}${resolved.promptSuffix}${HTML_INSTRUCTION}`, images: resolved.images };
79
- yield* streamTurnWithBusyRetry(agent, session, prompt, { label: "[telegram]", onCompleted, busyRetry });
79
+ yield* streamTurnWithBusyRetry(agent, { session }, prompt, { label: "[telegram]", onCompleted, busyRetry });
80
80
  }
@@ -5,8 +5,8 @@
5
5
  * "decide" and prompt-building half; telegram.ts wires it in and owns the stateful lifecycle. Kept
6
6
  * separate so this layer tests as plain functions and reads without the factory's noise.
7
7
  */
8
- import { BUFFER_LINE_MAX_CHARS } from "../context-buffer.js";
9
- import { REFERENT_MAX_CODE_POINTS, truncateCodePointPrefix } from "../text.js";
8
+ import { BUFFER_LINE_MAX_CHARS } from "../kit/context-buffer.js";
9
+ import { REFERENT_MAX_CODE_POINTS, truncateCodePointPrefix } from "../kit/text.js";
10
10
  /** The actionable message in an update (a fresh message or channel post). Edits (`edited_message` /
11
11
  * `edited_channel_post`) are deliberately NOT actionable: answering them re-answers every typo fix (a
12
12
  * duplicate reply per edit), so an edited message changes nothing — the standard bot behavior. The
@@ -6,7 +6,7 @@
6
6
  * message, works in groups and private (unlike sendMessageDraft, which is private/forum-topic only).
7
7
  */
8
8
  import type { AgentEvent } from "../../agent.ts";
9
- import { type ChannelFailure, defaultErrorMessage } from "../preview-kit.ts";
9
+ import { type ChannelFailure, defaultErrorMessage } from "../kit/preview-kit.ts";
10
10
  import { type Target } from "./telegram-api.ts";
11
11
  /** A terminal failure, as the channel hands it to `onError` — the shared channel shape. */
12
12
  export type TelegramFailure = ChannelFailure;
@@ -1,4 +1,4 @@
1
- import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createPreviewPump, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../preview-kit.js";
1
+ import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createPreviewPump, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../kit/preview-kit.js";
2
2
  import { log } from "../../log.js";
3
3
  import { TELEGRAM_MAX_TEXT, callApi, editMessageText, sendMessage } from "./telegram-api.js";
4
4
  export { defaultErrorMessage };
@@ -120,7 +120,11 @@ export async function streamReply(events, api, botToken, target, formatError, pr
120
120
  // below regardless.
121
121
  finalized = true;
122
122
  {
123
- const msg = formatError({ details: e.details, retryable: e.retryable }) ?? "";
123
+ const msg = formatError({
124
+ details: e.details,
125
+ retryable: e.retryable,
126
+ ...(e.code !== undefined ? { code: e.code } : {}),
127
+ }) ?? "";
124
128
  await finalize(api, botToken, target, messageId, msg).catch(() => { });
125
129
  }
126
130
  throw new Error(`agent failed: ${e.details} (retryable=${e.retryable})`);
@@ -1,4 +1,4 @@
1
- import type { ChannelModule } from "../../host/node.ts";
1
+ import type { ChannelModule } from "../../channel.ts";
2
2
  import { type TelegramMessage, type TelegramRoute, type TelegramUpdate, defaultTelegramRoute, telegramEnvelope } from "./parse.ts";
3
3
  import { type TelegramFailure } from "./preview.ts";
4
4
  export { defaultTelegramRoute, telegramEnvelope };
@@ -31,10 +31,10 @@ import { invokeTurn } from "./invoke-turn.js";
31
31
  import { collectAttachments, createContextBuffer } from "./context-buffer.js";
32
32
  import { attachmentSummary, defaultTelegramRoute, extractFiles, extractImages, fromLabel, messageText, ownFiles, ownImages, pickMessage, telegramEnvelope, } from "./parse.js";
33
33
  import { defaultErrorMessage, streamReply } from "./preview.js";
34
- import { ensureStateHome } from "../state.js";
35
- import { dispatchStop } from "../stop-command.js";
34
+ import { ensureStateHome } from "../kit/state.js";
35
+ import { dispatchStop } from "../kit/stop-command.js";
36
36
  import { callApi, editMessageText, sendMessage } from "./telegram-api.js";
37
- import { createTurnQueue } from "../turn-queue.js";
37
+ import { createTurnQueue } from "../kit/turn-queue.js";
38
38
  import { createTurnStore } from "./turn-store.js";
39
39
  // Re-export the public surface authored elsewhere, so `@fastagent-sh/fastagent/telegram` keeps one entry point.
40
40
  export { defaultTelegramRoute, telegramEnvelope };
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a
3
3
  * turn), its IO-boundary shape validator, and its arrival ordering, over the generic L1 turn store
4
- * (../turn-store.ts — the semantics live there: pre-ACK persist, replay on the next start, the poison-
4
+ * (../kit/turn-store.ts — the semantics live there: pre-ACK persist, replay on the next start, the poison-
5
5
  * turn execution ceiling, the fail-closed attempt bump).
6
6
  */
7
- import { type TurnStore } from "../turn-store.ts";
7
+ import { type TurnStore } from "../kit/turn-store.ts";
8
8
  /** An accepted turn's persisted intent — the SOURCE for the fields a runner needs to re-execute it
9
9
  * (telegram.ts's PendingTurn derives from this, so a new execution field added here propagates and
10
10
  * cannot silently drop from the persisted record). Minus the live `previewId` (a restart's queue
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a
3
3
  * turn), its IO-boundary shape validator, and its arrival ordering, over the generic L1 turn store
4
- * (../turn-store.ts — the semantics live there: pre-ACK persist, replay on the next start, the poison-
4
+ * (../kit/turn-store.ts — the semantics live there: pre-ACK persist, replay on the next start, the poison-
5
5
  * turn execution ceiling, the fail-closed attempt bump).
6
6
  */
7
- import { createTurnStore as createGenericTurnStore } from "../turn-store.js";
7
+ import { createTurnStore as createGenericTurnStore } from "../kit/turn-store.js";
8
8
  /** State files are an IO boundary: valid JSON of the WRONG SHAPE must degrade like a corrupt file
9
9
  * (warn + empty), not flow in as trusted data (mirrors context-buffer's isBufferEntry). */
10
10
  function isStoredTurn(t) {
@@ -11,7 +11,7 @@ import { dotEnvPath, loadDotEnv } from "../../env.js";
11
11
  import { resolveStateRoot } from "../../paths.js";
12
12
  import { SECRETS_DIRNAME } from "../../paths.js";
13
13
  import { detectRuntime, readPackageJson } from "../../runtime.js";
14
- import { isUnderDir } from "../../engines/pi/definition.js";
14
+ import { isUnderDir } from "../../paths.js";
15
15
  import { displayPath } from "../../paths.js";
16
16
  import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, } from "../../scaffold/add-channel.js";
17
17
  import { exists } from "../../paths.js";
@@ -76,7 +76,7 @@ export async function runDeploy(host, dirArg, opts) {
76
76
  failStartup(new Error(`deploy stopped: ${pre.gate}`));
77
77
  for (const m of pre.messages)
78
78
  console.error(`[fastagent] ${m.level}: ${m.text}`);
79
- const { channels, routeChannels, longConnectionChannels, hasTimeTriggers, modelAuth, authPath, container, port, extraSecrets, } = pre;
79
+ const { channels, routeChannels, longConnectionChannels, hasTimeTriggers, modelAuth, modelKeyInDefinition, authPath, container, port, extraSecrets, } = pre;
80
80
  const hasDeclaredChannels = routeChannels.length + longConnectionChannels.length > 0;
81
81
  // Docker: one app service + loopback port + state volume. `--tunnel` shapes the generated topology
82
82
  // with an optional Quick Tunnel service; `--run` alone decides whether Docker receives side effects.
@@ -126,6 +126,7 @@ export async function runDeploy(host, dirArg, opts) {
126
126
  port,
127
127
  requireTunnel: requestedTunnel,
128
128
  modelAuth,
129
+ modelKeyInDefinition,
129
130
  authPath,
130
131
  channels,
131
132
  longConnectionChannels,
@@ -176,6 +177,7 @@ export async function runDeploy(host, dirArg, opts) {
176
177
  workspace,
177
178
  name: serviceName,
178
179
  modelAuth,
180
+ modelKeyInDefinition,
179
181
  authPath,
180
182
  channels,
181
183
  longConnectionChannels,
@@ -278,6 +280,7 @@ export async function runDeploy(host, dirArg, opts) {
278
280
  agentPrefix: container.agentPrefix,
279
281
  name: acName,
280
282
  modelAuth,
283
+ modelKeyInDefinition,
281
284
  authPath,
282
285
  channels,
283
286
  extraSecrets,
@@ -371,6 +374,7 @@ export async function runDeploy(host, dirArg, opts) {
371
374
  agentPrefix: container.agentPrefix,
372
375
  appName,
373
376
  modelAuth,
377
+ modelKeyInDefinition,
374
378
  authPath,
375
379
  channels,
376
380
  longConnectionChannels,
@@ -468,9 +472,10 @@ function deployEnvironment(agentDir, channels) {
468
472
  * when present, yields an ephemeral URL that reuses the same webhook announcer as `dev --tunnel`.
469
473
  */
470
474
  async function runDeployDocker(params) {
471
- const { agentDir, workspace, composeFile, port, requireTunnel, modelAuth, authPath, channels, longConnectionChannels, extraSecrets, } = params;
475
+ const { agentDir, workspace, composeFile, port, requireTunnel, modelAuth, modelKeyInDefinition, authPath, channels, longConnectionChannels, extraSecrets, } = params;
472
476
  const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
473
477
  modelAuth,
478
+ modelKeyInDefinition,
474
479
  authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
475
480
  channels,
476
481
  longConnectionChannels,
@@ -511,7 +516,7 @@ async function runDeployDocker(params) {
511
516
  * behind the shared {@link spawnRunner} seam (spawned `fly`, cwd = the workspace so the build context is the whole workspace).
512
517
  */
513
518
  async function runDeployFly(params) {
514
- const { agentDir, workspace, agentPrefix, appName, modelAuth, authPath, channels, longConnectionChannels, flyTomlPath, extraSecrets, } = params;
519
+ const { agentDir, workspace, agentPrefix, appName, modelAuth, modelKeyInDefinition, authPath, channels, longConnectionChannels, flyTomlPath, extraSecrets, } = params;
515
520
  const fly = spawnRunner("fly", workspace);
516
521
  // Fail fast if flyctl is absent (spawn ENOENT → 127), with the install link — not a confusing auth gate.
517
522
  if ((await fly(["version"], { capture: true })).code === 127) {
@@ -520,6 +525,7 @@ async function runDeployFly(params) {
520
525
  const region = parseFlyRegion(await readFile(flyTomlPath, "utf8")) ?? "iad";
521
526
  const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
522
527
  modelAuth,
528
+ modelKeyInDefinition,
523
529
  authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
524
530
  channels,
525
531
  longConnectionChannels,
@@ -553,9 +559,10 @@ async function runDeployFly(params) {
553
559
  * file (secret values off argv) is created here — 0600, removed after the run either way.
554
560
  */
555
561
  async function runDeployAgentcore(params) {
556
- const { agentDir, workspace, agentPrefix, name, modelAuth, authPath, channels, extraSecrets, selfSchedule } = params;
562
+ const { agentDir, workspace, agentPrefix, name, modelAuth, modelKeyInDefinition, authPath, channels, extraSecrets, selfSchedule, } = params;
557
563
  const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
558
564
  modelAuth,
565
+ modelKeyInDefinition,
559
566
  authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
560
567
  channels,
561
568
  extraSecrets,
@@ -627,7 +634,7 @@ async function runDeployAgentcore(params) {
627
634
  * webhook) lives in {@link deployRailwayRun}; see there for why Railway differs from Fly.
628
635
  */
629
636
  async function runDeployRailway(params) {
630
- const { agentDir, workspace, name, modelAuth, authPath, channels, longConnectionChannels, extraSecrets, intoLinked, dockerfilePath, } = params;
637
+ const { agentDir, workspace, name, modelAuth, modelKeyInDefinition, authPath, channels, longConnectionChannels, extraSecrets, intoLinked, dockerfilePath, } = params;
631
638
  const railway = spawnRunner("railway", workspace);
632
639
  // Fail fast if the railway CLI is absent (spawn ENOENT → 127), with the install link.
633
640
  if ((await railway(["--version"], { capture: true })).code === 127) {
@@ -635,6 +642,7 @@ async function runDeployRailway(params) {
635
642
  }
636
643
  const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
637
644
  modelAuth,
645
+ modelKeyInDefinition,
638
646
  authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
639
647
  channels,
640
648
  longConnectionChannels,
@@ -6,15 +6,18 @@
6
6
  import { resolve } from "node:path";
7
7
  import { runDevSupervisor } from "../../dev-supervisor.js";
8
8
  import { loadDotEnv } from "../../env.js";
9
- import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
9
+ import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
10
+ import { reportModuleLoadFailures } from "../../log.js";
11
+ import { CODING_TOOL_NAMES } from "../../engines/pi/create.js";
10
12
  import { createPiAgentFromDir } from "../../engines/pi/open.js";
13
+ import { mountAgentService } from "../../service.js";
11
14
  import { setLogLevel } from "../../log.js";
12
15
  import { logAgentLoop } from "../../observe.js";
13
16
  import { installProxyFetch } from "../../proxy.js";
14
17
  import { workspaceHint } from "../../paths.js";
15
18
  import { bindAddress } from "../../bind.js";
16
19
  import { failStartup, placementOrExit } from "../fail.js";
17
- import { assertTunnelBindable, maybeTunnel, mountSessionControl, routesFor, serve, startSchedules } from "../serve.js";
20
+ import { SHUTDOWN_GRACE_MS, assertTunnelBindable, maybeTunnel, reportServing, serve } from "../serve.js";
18
21
  import { parseBind, parsePort, reportAuth, reportLine, resolveFirstRunModel, reportWorkspaceHint } from "../shared.js";
19
22
  export async function runDev(dirArg, opts) {
20
23
  const dir = resolve(dirArg);
@@ -56,26 +59,34 @@ async function serveOnce(dir, opts) {
56
59
  reportWorkspaceHint(workspaceHint(a));
57
60
  reportLine("config", a.configPath ?? "(none)");
58
61
  reportLine("model", `${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
59
- await reportAuth(a.modelSpec, a.authPath);
62
+ await reportAuth(a.agentDir, a.modelSpec, a.authPath);
60
63
  reportAgentsSkillsTools(a);
61
64
  // Trace each turn's agent loop (tool calls + reply) to the log at debug level — shown in dev, gated
62
65
  // out in start (level info), keeping end-user content out of production logs. Wired in both postures.
63
- const traced = logAgentLoop(a.agent);
64
- const routed = await routesFor(a.agentDir, traced, a.stateRoot, a.sessionControl).catch(failStartup);
65
66
  // `http.host` enters here the way the flag enters `parseBind` — through `bindAddress`, so a
66
67
  // configured `localhost` is an ADDRESS by the time anything binds, renders or dials it.
67
68
  const configured = a.config.http?.host;
68
69
  const host = bindFlag ?? (configured === undefined ? undefined : bindAddress(configured));
69
70
  assertTunnelBindable(host, opts.tunnel ?? false, bindFlag ? "flag" : "config");
70
- const withControl = mountSessionControl(routed.routes, a.sessionControl, a.stateRoot, {
71
- tunnel: opts.tunnel ?? false,
72
- agent: traced, // the remote data plane (POST /control/invoke) drives the SAME traced agent
73
- host,
74
- });
75
- await startSchedules(a.agentDir, traced, a.stateRoot, a.config.selfSchedule ?? false);
76
- serve({ ...routed, routes: withControl.routes }, { port: portFlag ?? a.config.http?.port ?? 8787, host }, (p) => {
77
- withControl.announce(p);
78
- maybeTunnel(a.agentDir, routed.routeChannels, p, opts.tunnel ?? false, a.stateRoot);
71
+ // The SAME assembly an embedder gets from `createAgentService` — channels, control plane,
72
+ // schedules, long connections. `dev` opens the directory itself only because its startup report
73
+ // prints the opened values before anything mounts.
74
+ const service = await mountAgentService(a, {
75
+ // Trace each turn's agent loop (tool calls + reply) to the log at debug level — shown in dev,
76
+ // gated out in start (level info), keeping end-user content out of production logs.
77
+ wrapAgent: logAgentLoop,
78
+ closeTimeoutMs: SHUTDOWN_GRACE_MS,
79
+ control: { tunnel: opts.tunnel ?? false, ...(host !== undefined ? { host } : {}) },
80
+ onChannelClosed: (name, error) => failStartup(new Error(`${name} ${error === undefined ? "closed unexpectedly" : `failed: ${String(error)}`}`)),
81
+ }).catch(failStartup);
82
+ serve(service.handler, { port: portFlag ?? a.config.http?.port ?? 8787, host }, {
83
+ ready: service.ready,
84
+ onListening: (p) => {
85
+ reportServing(service, host, p);
86
+ service.announce(p);
87
+ maybeTunnel(a.agentDir, service.channels.routes, p, opts.tunnel ?? false, a.stateRoot);
88
+ },
89
+ onShutdown: () => service.close(),
79
90
  });
80
91
  }
81
92
  /** The agents/skills/tools/collisions report lines. */
@@ -84,6 +95,7 @@ function reportAgentsSkillsTools(a) {
84
95
  if (a.definition.persona)
85
96
  reportLine("persona", "persona.md");
86
97
  reportLine("skills", a.definition.skills.map((s) => s.name).join(", ") || "(none)");
98
+ reportLine("codingTools", CODING_TOOL_NAMES.join(", "));
87
99
  if (a.toolNames.length > 0)
88
100
  reportLine("tools", a.toolNames.join(", "));
89
101
  if (a.deferredToolNames.length > 0) {
@@ -7,7 +7,7 @@
7
7
  import { join, resolve } from "node:path";
8
8
  import { loadDotEnv } from "../../env.js";
9
9
  import { displayPath } from "../../paths.js";
10
- import { reportModuleLoadFailures } from "../../engines/pi/report.js";
10
+ import { reportModuleLoadFailures } from "../../log.js";
11
11
  import { createPiAgentFromDir } from "../../engines/pi/open.js";
12
12
  import { runInvokeStream } from "../invoke-stream.js";
13
13
  import { installProxyFetch } from "../../proxy.js";
@@ -37,7 +37,7 @@ export async function runFire(name, dirArg, opts) {
37
37
  authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
38
38
  }).catch(failStartup);
39
39
  console.error(`[fastagent] fire: ${name} (${modelSpec})`);
40
- await reportAuth(modelSpec, authPath);
40
+ await reportAuth(placement.agentDir, modelSpec, authPath);
41
41
  const exitCode = await runInvokeStream(agent.invoke({ session: scheduleSession(name) }, { text: schedule.prompt }), (text) => process.stdout.write(text), (line) => console.error(line));
42
42
  process.stdout.write("\n");
43
43
  process.exit(exitCode);
@@ -1,12 +1,14 @@
1
1
  /** `fastagent info [dir] [--json]`: print what the directory ASSEMBLES into, WITHOUT booting a server. Read-only. */
2
2
  import { resolve } from "node:path";
3
3
  import { loadDotEnv } from "../../env.js";
4
- import { discoverChannelFiles } from "../../engines/pi/channel.js";
5
- import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModelSpec, resolveSessionsDirOverride, } from "../../engines/pi/config.js";
4
+ import { discoverChannelFiles } from "../../channels/discover.js";
5
+ import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModel, resolveModelSpec, resolveSessionsDirOverride, } from "../../engines/pi/config.js";
6
+ import { createPiModelRuntime } from "../../engines/pi/models.js";
6
7
  import { resolveStateRoot, workspaceHint } from "../../paths.js";
7
- import { resolveAgentTools } from "../../engines/pi/create.js";
8
+ import { CODING_TOOL_NAMES, resolveAgentTools } from "../../engines/pi/create.js";
8
9
  import { loadAgentDefinition } from "../../engines/pi/definition.js";
9
- import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
10
+ import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
11
+ import { reportModuleLoadFailures } from "../../log.js";
10
12
  import { log } from "../../log.js";
11
13
  import { nextRun } from "../../schedule/cron.js";
12
14
  import { loadSchedules } from "../../schedule/discover.js";
@@ -24,7 +26,7 @@ export async function runInfo(dirArg, opts) {
24
26
  // tool), is isolated the same way everywhere (G2): info, dev, AND start report it and keep going with
25
27
  // the tools that loaded. The `error`/`.catch` below only fires for a whole-load fault (an unreadable
26
28
  // tools/ dir), not a single bad file.
27
- const tools = await resolveAgentTools(config, agentDir)
29
+ const tools = await resolveAgentTools(config, agentDir, workspace)
28
30
  .then((r) => ({
29
31
  names: r.toolNames,
30
32
  deferred: r.deferredToolNames,
@@ -55,13 +57,29 @@ export async function runInfo(dirArg, opts) {
55
57
  const stateRoot = resolveStateRoot(agentDir);
56
58
  const sessionsDir = resolveSessionsDirOverride(opts.sessionsDir) ?? defaultSessionsDir(stateRoot);
57
59
  const authPath = resolveAuthPath(agentDir, opts.authPath); // flag > FASTAGENT_AUTH_PATH > default — the one owner
60
+ // RESOLVE the spec, do not just echo it: a spec is only real once its provider/model exist in the
61
+ // agent's own surface (built-ins + its models.json), which is exactly what a custom endpoint changes.
62
+ // Reporting a healthy-looking spec that `dev`/`start` then reject is the failure this pre-empts.
63
+ // Reported as DATA rather than thrown — a broken agent is what `info` is for — and read-only: the
64
+ // runtime reads models.json without creating anything (its catalog cache is written on refresh, and
65
+ // there is none here).
66
+ const modelError = modelSpec
67
+ ? await createPiModelRuntime({ agentDir, authPath })
68
+ .then((models) => {
69
+ resolveModel(models, modelSpec);
70
+ return undefined;
71
+ })
72
+ .catch((error) => error.message)
73
+ : undefined;
58
74
  if (opts.json) {
59
75
  console.log(JSON.stringify({
60
76
  agentDir,
61
77
  workspace,
62
78
  configPath: configPath ?? null,
63
79
  model: modelSpec ?? null,
80
+ modelError: modelError ?? null,
64
81
  thinkingLevel: config.thinkingLevel ?? null,
82
+ codingTools: [...CODING_TOOL_NAMES],
65
83
  context: definition.contextFiles.map((f) => f.path),
66
84
  persona: definition.persona !== undefined,
67
85
  skills: definition.skills.map((skill) => ({ name: skill.name, description: skill.description })),
@@ -85,6 +103,8 @@ export async function runInfo(dirArg, opts) {
85
103
  // One padded label writer: hand-spaced labels drifted out of alignment the moment a longer one
86
104
  // (agent/workspace/selfSchedule) joined the report.
87
105
  const line = (label, value) => console.log(`${`${label}:`.padEnd(13)} ${value}`);
106
+ /** A continuation under the previous line, aligned to the same column (no label, so no bare colon). */
107
+ const cont = (value) => console.log(`${"".padEnd(13)} ${value}`);
88
108
  line("agent", agentDir);
89
109
  line("workspace", workspace);
90
110
  const hint = workspaceHint({ agentDir, workspace });
@@ -92,8 +112,11 @@ export async function runInfo(dirArg, opts) {
92
112
  line("hint", hint);
93
113
  line("config", configPath ?? "(none)");
94
114
  line("model", modelSpec ?? "(not set — pass --model, set FASTAGENT_MODEL, or config.model)");
115
+ if (modelError)
116
+ cont(`⚠ does not resolve: ${modelError}`);
95
117
  if (config.thinkingLevel)
96
118
  line("thinking", config.thinkingLevel);
119
+ line("codingTools", CODING_TOOL_NAMES.join(", "));
97
120
  line("context", definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
98
121
  line("persona", definition.persona ? "persona.md" : "(none)");
99
122
  line("skills", definition.skills.map((skill) => skill.name).join(", ") || "(none)");
@@ -20,7 +20,7 @@ export async function runInvoke(message, dirArg, opts) {
20
20
  // BOTH directories, like dev/start: from the workspace, `placement.workspace` alone equals the dir you
21
21
  // typed, so it cannot tell you which agent actually ran.
22
22
  console.error(`[fastagent] invoke: ${placement.agentDir} (workspace ${placement.workspace}, ${modelSpec})`);
23
- await reportAuth(modelSpec, authPath);
23
+ await reportAuth(placement.agentDir, modelSpec, authPath);
24
24
  // Fresh session per invoke (one-shot, no resume). runInvokeStream maps events→IO: reply→stdout,
25
25
  // tool/failure→stderr, exit 1 iff the turn failed (so CI can gate on it).
26
26
  const exitCode = await runInvokeStream(agent.invoke({ session: randomUUID() }, { text: message }), (text) => process.stdout.write(text), (line) => console.error(line));
@@ -6,7 +6,7 @@
6
6
  import { resolve } from "node:path";
7
7
  import { loadDotEnv } from "../../env.js";
8
8
  import { resolveStateRoot } from "../../paths.js";
9
- import { reportModuleLoadFailures } from "../../engines/pi/report.js";
9
+ import { reportModuleLoadFailures } from "../../log.js";
10
10
  import { readRuns } from "../../schedule/audit.js";
11
11
  import { nextRun } from "../../schedule/cron.js";
12
12
  import { loadSchedules } from "../../schedule/discover.js";