@fastagent-sh/fastagent 0.24.1 → 0.24.3

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 (53) hide show
  1. package/dist/agent.d.ts +6 -0
  2. package/dist/agent.js +6 -0
  3. package/dist/cli/commands/add.js +1 -1
  4. package/dist/cli/commands/chat.js +1 -1
  5. package/dist/cli/commands/deploy.js +2 -2
  6. package/dist/cli/commands/dev.js +2 -2
  7. package/dist/cli/commands/info.js +12 -5
  8. package/dist/cli/commands/invoke.js +2 -3
  9. package/dist/cli/commands/routine-run.js +4 -5
  10. package/dist/cli/commands/start.js +2 -2
  11. package/dist/cli/kernel.js +1 -1
  12. package/dist/cli/program.js +4 -3
  13. package/dist/cli/shared.d.ts +28 -2
  14. package/dist/cli/shared.js +36 -13
  15. package/dist/core.d.ts +1 -1
  16. package/dist/core.js +5 -4
  17. package/dist/deploy/build-context.js +1 -1
  18. package/dist/deploy/preflight.js +9 -1
  19. package/dist/dev-supervisor.js +1 -1
  20. package/dist/engines/pi/agent-session-factory.d.ts +21 -5
  21. package/dist/engines/pi/agent-session-factory.js +134 -17
  22. package/dist/engines/pi/config.d.ts +6 -0
  23. package/dist/engines/pi/config.js +8 -0
  24. package/dist/engines/pi/create.d.ts +25 -30
  25. package/dist/engines/pi/create.js +64 -66
  26. package/dist/engines/pi/definition.d.ts +44 -28
  27. package/dist/engines/pi/definition.js +123 -30
  28. package/dist/engines/pi/invoke-session.d.ts +4 -1
  29. package/dist/engines/pi/invoke-session.js +12 -6
  30. package/dist/engines/pi/machine.d.ts +1 -2
  31. package/dist/engines/pi/machine.js +10 -1
  32. package/dist/engines/pi/open.d.ts +11 -7
  33. package/dist/engines/pi/open.js +13 -13
  34. package/dist/engines/pi/report.d.ts +7 -8
  35. package/dist/engines/pi/report.js +23 -5
  36. package/dist/engines/pi/session-builder.js +6 -2
  37. package/dist/engines/pi/session-control.d.ts +3 -2
  38. package/dist/engines/pi/session-control.js +23 -9
  39. package/dist/engines/pi/session-markers.d.ts +1 -1
  40. package/dist/engines/pi/session-markers.js +1 -1
  41. package/dist/engines/pi/session-settings.d.ts +14 -3
  42. package/dist/engines/pi/session-settings.js +20 -1
  43. package/dist/engines/pi/turn-kit.d.ts +31 -1
  44. package/dist/engines/pi/turn-kit.js +54 -1
  45. package/dist/paths.d.ts +1 -1
  46. package/dist/paths.js +12 -1
  47. package/dist/pi.d.ts +1 -1
  48. package/dist/scaffold/init.js +2 -2
  49. package/dist/scaffold/templates/APPEND_SYSTEM.md +12 -0
  50. package/dist/scaffold/templates/fastagent.config.ts +1 -1
  51. package/dist/session.d.ts +10 -0
  52. package/package.json +1 -1
  53. package/dist/scaffold/templates/persona.md +0 -12
package/dist/agent.d.ts CHANGED
@@ -91,6 +91,12 @@ export declare const SESSION_BUSY_CODE = "session_busy";
91
91
  * never replayed as a fresh turn on restart.
92
92
  */
93
93
  export declare const ABORTED_CODE = "aborted";
94
+ /**
95
+ * The `failed.code` set when a turn has no model to run on: its session records none of its own, and the agent sets
96
+ * no default. Not retryable as is: giving the session a model (session control's `update({ model })`) or the agent a
97
+ * default is what changes the answer.
98
+ */
99
+ export declare const MISSING_MODEL_CODE = "missing_model";
94
100
  /**
95
101
  * One turn = one invoke, returning a single async event stream. The stream MUST terminate with exactly one of
96
102
  * `completed` / `failed`, or be cancelled by the caller (no terminal event).
package/dist/agent.js CHANGED
@@ -13,3 +13,9 @@ export const SESSION_BUSY_CODE = "session_busy";
13
13
  * never replayed as a fresh turn on restart.
14
14
  */
15
15
  export const ABORTED_CODE = "aborted";
16
+ /**
17
+ * The `failed.code` set when a turn has no model to run on: its session records none of its own, and the agent sets
18
+ * no default. Not retryable as is: giving the session a model (session control's `update({ model })`) or the agent a
19
+ * default is what changes the answer.
20
+ */
21
+ export const MISSING_MODEL_CODE = "missing_model";
@@ -238,5 +238,5 @@ export async function runAddSkill(source, dirArg, opts) {
238
238
  if (hasScripts) {
239
239
  console.error(` warn: this skill ships scripts/ (executable code that runs in your agent) — review it before deploying`);
240
240
  }
241
- console.error(` next: mention "${name}" in persona.md so the model knows when to use it; then \`fastagent dev\``);
241
+ console.error(` next: mention "${name}" in APPEND_SYSTEM.md so the model knows when to use it; then \`fastagent dev\``);
242
242
  }
@@ -10,5 +10,5 @@ export async function runChat(dirArg, opts) {
10
10
  process.chdir(placement.workspace);
11
11
  // Lazy-import: chat pulls pi's interactive TUI module graph; headless start/dev never need it.
12
12
  const { runPiChat } = await import("../../engines/pi/chat.js");
13
- await runPiChat(placement.agentDir, { model: opts.model }).catch(failStartup);
13
+ await runPiChat(placement.agentDir, { model: placement.modelSpec }).catch(failStartup);
14
14
  }
@@ -1,7 +1,7 @@
1
1
  import { preflightDeploy } from "../../deploy/preflight.js";
2
2
  import { loadConfig } from "../../engines/pi/config.js";
3
3
  import { failStartup, failUsage } from "../fail.js";
4
- import { enterAgentCommand } from "../shared.js";
4
+ import { enterAgentDirectory } from "../shared.js";
5
5
  import { agentcoreHost } from "./deploy/agentcore.js";
6
6
  import { dockerHost } from "./deploy/docker.js";
7
7
  import { flyHost } from "./deploy/fly.js";
@@ -56,7 +56,7 @@ export async function runDeploy(host, dirArg, opts) {
56
56
  failUsage(`deploy stopped: --tunnel is supported only by the local Docker target`);
57
57
  }
58
58
  // The picker's write-back lands the model in fastagent.config.ts.
59
- const placement = await enterAgentCommand(dirArg, opts);
59
+ const placement = await enterAgentDirectory(dirArg, opts);
60
60
  // ONE deploy semantic: bake the WORKSPACE (WYSIWYG).
61
61
  const { agentDir, workspace } = placement;
62
62
  const { config } = await loadConfig(agentDir).catch(failStartup);
@@ -36,11 +36,11 @@ async function serveOnce(placement, opts) {
36
36
  const tunnel = opts.tunnel ?? false;
37
37
  assertTunnelBindable(host, tunnel);
38
38
  const a = await createPiAgentFromDir(placement.agentDir, {
39
- model: opts.model,
39
+ model: placement.modelSpec,
40
40
  serving: true, // long-running serve: the scheduler poller runs (and the wake tool is mounted)
41
41
  }).catch(failStartup);
42
42
  // The same report `start` prints; `config:` is dev's own extra (see reportAssembly on the asymmetry).
43
- await reportAssembly(a, { beforeModel: [["config", a.configPath ?? "(none)"]] });
43
+ await reportAssembly({ ...a, modelSpec: placement.modelSpec }, { beforeModel: [["config", a.configPath ?? "(none)"]] });
44
44
  // The SAME assembly an embedder gets from `createAgentService` — channels, control plane, schedules, long
45
45
  // connections.
46
46
  const service = await mountAgentService(withRunOverrides(a, opts), cliMountOptions(logAgentLoop)).catch(failStartup);
@@ -8,7 +8,7 @@ import { agentModels } from "../../engines/pi/agent-models.js";
8
8
  import { resolveSessionsDir, resolveStateRoot } from "../../paths.js";
9
9
  import { CODING_TOOL_NAMES, resolveAgentTools } from "../../engines/pi/create.js";
10
10
  import { loadAgentDefinition } from "../../engines/pi/definition.js";
11
- import { describeIndirectTools, reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
11
+ import { describeIndirectTools, describePrompt, reportFindingsIfChanged, reportToolCollisions, } from "../../engines/pi/report.js";
12
12
  import { allSecrets, describeSecrets, missingSecrets } from "../../declared-secrets.js";
13
13
  import { log } from "../../log.js";
14
14
  import { reportModuleLoadFailures } from "../../loader.js";
@@ -25,8 +25,10 @@ export async function runInfo(dirArg, opts) {
25
25
  // agentDir = where the agent lives (definition + config + machinery); workspace = what it works ON (its cwd, whose
26
26
  // AGENTS.md ancestors are ② context).
27
27
  const definition = await loadAgentDefinition(agentDir, { cwd: workspace }).catch(failStartup);
28
- // What this agent HAS: the definition's skills plus the ones its machine lends (machine.ts).
29
- const skills = withMachine(definition.skills, (await readMachine(workspace)).skills);
28
+ // What this agent HAS: the definition's skills and prompt templates plus the ones its machine lends (machine.ts).
29
+ const machineResources = await readMachine(workspace);
30
+ const skills = withMachine(definition.skills, machineResources.skills);
31
+ const prompts = withMachine(definition.prompts, machineResources.prompts);
30
32
  // A tool that fails to load, for any reason (a missing dep, a top-level throw, or just not being a tool), is
31
33
  // isolated the same way everywhere (G2).
32
34
  const tools = await resolveAgentTools(config, agentDir, workspace)
@@ -104,8 +106,10 @@ export async function runInfo(dirArg, opts) {
104
106
  thinkingLevel: config.thinkingLevel ?? null,
105
107
  codingTools: [...CODING_TOOL_NAMES],
106
108
  context: definition.contextFiles.map((f) => f.path),
107
- persona: definition.persona !== undefined,
109
+ systemPrompt: definition.systemPrompt?.path ?? null,
110
+ appendSystemPrompt: definition.appendSystemPrompt?.path ?? null,
108
111
  skills: skills.map((skill) => ({ name: skill.name, description: skill.description })),
112
+ prompts: prompts.map((prompt) => ({ name: prompt.name, description: prompt.description ?? null })),
109
113
  tools: tools.names,
110
114
  indirectTools: tools.indirect,
111
115
  toolError: tools.error ?? null,
@@ -119,6 +123,8 @@ export async function runInfo(dirArg, opts) {
119
123
  fallbackAuthPath: auth.fallback ?? null,
120
124
  diagnostics: definition.diagnostics,
121
125
  skillCollisions: definition.collisions,
126
+ shadowed: definition.shadowed,
127
+ ignored: definition.ignored,
122
128
  toolCollisions: tools.collisions,
123
129
  toolFailures: tools.failures,
124
130
  declaredSecrets,
@@ -142,8 +148,9 @@ export async function runInfo(dirArg, opts) {
142
148
  line("thinking", config.thinkingLevel);
143
149
  line("codingTools", CODING_TOOL_NAMES.join(", "));
144
150
  line("context", definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
145
- line("persona", definition.persona ? "persona.md" : "(none)");
151
+ line("prompt", describePrompt(definition));
146
152
  line("skills", skills.map((skill) => skill.name).join(", ") || "(none)");
153
+ line("prompts", prompts.map((prompt) => prompt.name).join(", ") || "(none)");
147
154
  line("tools", tools.error ? "(could not load — see warning below)" : tools.names.join(", ") || "(none)");
148
155
  if (tools.indirect.length > 0)
149
156
  line("indirect", describeIndirectTools(tools.indirect));
@@ -6,9 +6,8 @@ import { failStartup } from "../fail.js";
6
6
  import { enterAgentCommand, reportAuth } from "../shared.js";
7
7
  export async function runInvoke(message, dirArg, opts) {
8
8
  const placement = await enterAgentCommand(dirArg, opts);
9
- const { agent, modelSpec, models } = await createPiAgentFromDir(placement.agentDir, {
10
- model: opts.model,
11
- }).catch(failStartup);
9
+ const { modelSpec } = placement;
10
+ const { agent, models } = await createPiAgentFromDir(placement.agentDir, { model: modelSpec }).catch(failStartup);
12
11
  // BOTH directories, like dev/start: which agent ran, and what it worked on.
13
12
  console.error(`[fastagent] invoke: ${placement.agentDir} (workspace ${placement.workspace}, ${modelSpec})`);
14
13
  await reportAuth(models, modelSpec);
@@ -10,9 +10,9 @@ import { runInvokeStream } from "../invoke-stream.js";
10
10
  import { loadRoutines } from "../../schedule/discover.js";
11
11
  import { routineSession } from "../../schedule/routine.js";
12
12
  import { failStartup, gateSecretsOrExit } from "../fail.js";
13
- import { enterAgentCommand, reportAuth } from "../shared.js";
13
+ import { enterAgentDirectory, reportAuth, requireDefaultModel } from "../shared.js";
14
14
  export async function runRoutine(name, dirArg, opts) {
15
- const placement = await enterAgentCommand(dirArg, opts);
15
+ const placement = await enterAgentDirectory(dirArg, opts);
16
16
  // Routines are agent surface — discovered where dev/start/`routine list` do (the agent dir), so this command
17
17
  // sees the same set the clock serves.
18
18
  const { routines, secrets, failures } = await loadRoutines(placement.agentDir).catch(failStartup);
@@ -36,9 +36,8 @@ export async function runRoutine(name, dirArg, opts) {
36
36
  // were printed above: its guarantee must not depend on this call site remembering (a repeated line
37
37
  // on the refusal path is the cheaper failure).
38
38
  gateSecretsOrExit({ declared: secrets, failures, owner: name });
39
- const { agent, modelSpec, models } = await createPiAgentFromDir(placement.agentDir, {
40
- model: opts.model,
41
- }).catch(failStartup);
39
+ const { modelSpec } = requireDefaultModel(placement);
40
+ const { agent, models } = await createPiAgentFromDir(placement.agentDir, { model: modelSpec }).catch(failStartup);
42
41
  console.error(`[fastagent] routine run: ${name} (${modelSpec})`);
43
42
  await reportAuth(models, modelSpec);
44
43
  const exitCode = await runInvokeStream(agent.invoke({ session: routineSession(name) }, { text: routine.prompt }), (text) => process.stdout.write(text), (line) => console.error(line));
@@ -125,11 +125,11 @@ export async function openStartService(dirArg, opts) {
125
125
  export async function openPreparedStartService(dirArg, opts) {
126
126
  const placement = await enterAgentCommand(dirArg, opts);
127
127
  const opened = await createPiAgentFromDir(placement.agentDir, {
128
- model: opts.model,
128
+ model: placement.modelSpec,
129
129
  serving: true,
130
130
  });
131
131
  const { agent, agentDir, config, stateRoot, sessionsDir } = opened;
132
- await reportAssembly(opened, {
132
+ await reportAssembly({ ...opened, modelSpec: placement.modelSpec }, {
133
133
  afterTools: [
134
134
  ["state", stateRoot],
135
135
  ["sessions", sessionsDir],
@@ -9,7 +9,7 @@ const specOf = new WeakMap();
9
9
  export function buildProgram(specs, options = {}) {
10
10
  const exit = options.exit ?? ((code) => process.exit(code));
11
11
  const program = new Command("fastagent");
12
- program.description("Serve a file-defined agent — persona.md, skills/, tools/, channels/ — as a live service.");
12
+ program.description("Serve a file-defined agent — SYSTEM.md, skills/, tools/, channels/ — as a live service.");
13
13
  // The exit-code policy: commander throws only for parse-level events — help/version displays carry exitCode 0 (→
14
14
  // 0), everything else it rejects is a usage error (→ 2).
15
15
  program.exitOverride((err) => exit(err.exitCode === 0 ? 0 : 2));
@@ -44,7 +44,7 @@ const init = {
44
44
  description: "Scaffold a runnable agent and run npm install. The agent ALWAYS goes into a subdirectory of dir " +
45
45
  "(default .): ./fastagent/, or --agent-dir <name> — the rest of the directory gets zero writes, and " +
46
46
  "it is the WORKSPACE the agent works ON when you point fastagent there. Content is a " +
47
- "self-iterating agent: persona.md (its identity), a writing-great-skills " +
47
+ "self-iterating agent: APPEND_SYSTEM.md (its standing instructions), a writing-great-skills " +
48
48
  "example skill, a fetch-url code tool, config, package.json, .gitignore. An existing AGENTS.md is " +
49
49
  "kept as project context.",
50
50
  args: [DIR_ARG],
@@ -70,7 +70,8 @@ const init = {
70
70
  const dev = {
71
71
  name: "dev",
72
72
  summary: "serve the agent locally, restarting on code edits",
73
- description: "Assemble the agent in dir (default .) and serve a local HTTP channel. persona.md/AGENTS.md/skills " +
73
+ description: "Assemble the agent in dir (default .) and serve a local HTTP channel. SYSTEM.md/APPEND_SYSTEM.md/" +
74
+ "AGENTS.md/skills/prompts " +
74
75
  "are re-read every turn (edits go live next turn); edits to code inputs — tools/, channels/, " +
75
76
  "fastagent.config.ts, package.json, .secrets/.env — restart the worker. Files the agent writes as " +
76
77
  "work product never trigger a restart.",
@@ -112,7 +113,7 @@ const chat = {
112
113
  const info = {
113
114
  name: "info",
114
115
  summary: "print what the directory assembles into, without serving",
115
- description: "Print what dir (default .) ASSEMBLES into — model, persona, context files (AGENTS.md), skills, " +
116
+ description: "Print what dir (default .) ASSEMBLES into — model, prompt, context files (AGENTS.md), skills, " +
116
117
  "tools (+ collisions), channels, schedules, sessions, load diagnostics — WITHOUT serving. " +
117
118
  "Read-only (never creates sessions / writes .gitignore); an unset model is reported, not fatal. " +
118
119
  "Run it first when something looks off.",
@@ -4,11 +4,37 @@ import { type IndirectTool } from "../engines/pi/create.ts";
4
4
  import type { LoadedDefinition } from "../engines/pi/definition.ts";
5
5
  import type { ToolCollision } from "../engines/pi/tool.ts";
6
6
  import { type ResolvedPlacement } from "../paths.ts";
7
- /** How every command that runs the model enters its agent directory, in the one order that works. */
7
+ /**
8
+ * How a command enters its agent directory, in the one order that works: placement, the agent's environment, then the
9
+ * first-run model picker. Returns the default model that then resolves, if any. `deploy` uses this directly: it gates
10
+ * a missing model itself, by what will ship.
11
+ */
12
+ export declare function enterAgentDirectory(dirArg: string, opts: {
13
+ model?: string;
14
+ input?: boolean;
15
+ }): Promise<ResolvedPlacement & {
16
+ modelSpec?: string;
17
+ }>;
18
+ /**
19
+ * How every command that RUNS the agent on this machine enters it (dev, start, invoke, routine run, chat):
20
+ * {@link enterAgentDirectory}, plus a default model. The agent opens without one, but a process here would then fail
21
+ * every new conversation, so it stops at startup with {@link missingDefaultModel} instead.
22
+ */
8
23
  export declare function enterAgentCommand(dirArg: string, opts: {
9
24
  model?: string;
10
25
  input?: boolean;
11
- }): Promise<ResolvedPlacement>;
26
+ }): Promise<ResolvedPlacement & {
27
+ modelSpec: string;
28
+ }>;
29
+ /**
30
+ * The startup half of {@link enterAgentCommand}, for the one command that checks something of its own in between:
31
+ * `routine run`, whose unknown name or unset secret is the more basic fault and is reported first.
32
+ */
33
+ export declare function requireDefaultModel(placement: ResolvedPlacement & {
34
+ modelSpec?: string;
35
+ }): ResolvedPlacement & {
36
+ modelSpec: string;
37
+ };
12
38
  /** What the startup report reads off an opened directory. */
13
39
  export interface ReportableAssembly {
14
40
  agentDir: string;
@@ -6,26 +6,48 @@ import { readFile, writeFile } from "node:fs/promises";
6
6
  import { relative, resolve } from "node:path";
7
7
  import { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
8
8
  import { buildModelPickerOptions } from "./models-view.js";
9
- import { isValidPort, listModels, loadConfig, providerOf, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
9
+ import { isValidPort, listModels, loadConfig, missingDefaultModel, providerOf, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
10
10
  import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
11
11
  import { readMachine, withMachine } from "../engines/pi/machine.js";
12
12
  import { providerAuthStatuses } from "../engines/pi/models.js";
13
13
  import { agentModels } from "../engines/pi/agent-models.js";
14
14
  import { formatAuthReport } from "./auth-view.js";
15
15
  import { CODING_TOOL_NAMES } from "../engines/pi/create.js";
16
- import { describeIndirectTools, reportFindingsIfChanged, reportToolCollisions } from "../engines/pi/report.js";
16
+ import { describeIndirectTools, describePrompt, reportFindingsIfChanged, reportToolCollisions, } from "../engines/pi/report.js";
17
17
  import { isDeployedWorkspace } from "../paths.js";
18
18
  import { log } from "../log.js";
19
19
  import { enterAgentEnv } from "../env.js";
20
20
  import { openExternalUrl } from "../open-url.js";
21
21
  import { bindAddress, isBindAddress } from "../bind.js";
22
22
  import { failStartup, failUsage, placementOrExit } from "./fail.js";
23
- /** How every command that runs the model enters its agent directory, in the one order that works. */
24
- export async function enterAgentCommand(dirArg, opts) {
23
+ /**
24
+ * How a command enters its agent directory, in the one order that works: placement, the agent's environment, then the
25
+ * first-run model picker. Returns the default model that then resolves, if any. `deploy` uses this directly: it gates
26
+ * a missing model itself, by what will ship.
27
+ */
28
+ export async function enterAgentDirectory(dirArg, opts) {
25
29
  const placement = placementOrExit(resolve(dirArg));
26
30
  enterAgentEnv(placement.agentDir);
27
- await resolveFirstRunModel(placement, opts);
28
- return placement;
31
+ const modelSpec = await resolveFirstRunModel(placement, opts);
32
+ return { ...placement, ...(modelSpec ? { modelSpec } : {}) };
33
+ }
34
+ /**
35
+ * How every command that RUNS the agent on this machine enters it (dev, start, invoke, routine run, chat):
36
+ * {@link enterAgentDirectory}, plus a default model. The agent opens without one, but a process here would then fail
37
+ * every new conversation, so it stops at startup with {@link missingDefaultModel} instead.
38
+ */
39
+ export async function enterAgentCommand(dirArg, opts) {
40
+ return requireDefaultModel(await enterAgentDirectory(dirArg, opts));
41
+ }
42
+ /**
43
+ * The startup half of {@link enterAgentCommand}, for the one command that checks something of its own in between:
44
+ * `routine run`, whose unknown name or unset secret is the more basic fault and is reported first.
45
+ */
46
+ export function requireDefaultModel(placement) {
47
+ const { modelSpec } = placement;
48
+ if (!modelSpec)
49
+ failStartup(missingDefaultModel());
50
+ return { ...placement, modelSpec };
29
51
  }
30
52
  /** The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). */
31
53
  function reportLine(label, value) {
@@ -40,8 +62,7 @@ export async function reportAssembly(a, extras = {}) {
40
62
  reportLine("model", `${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
41
63
  await reportAuth(a.models, a.modelSpec);
42
64
  reportLine("context", a.definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
43
- if (a.definition.persona)
44
- reportLine("persona", "persona.md");
65
+ reportLine("prompt", describePrompt(a.definition));
45
66
  // What this agent HAS — the definition's skills and the ones its machine lends (machine.ts).
46
67
  const skills = withMachine(a.definition.skills, (await readMachine(a.workspace)).skills);
47
68
  reportLine("skills", skills.map((s) => s.name).join(", ") || "(none)");
@@ -108,21 +129,23 @@ export async function reportAuth(models, modelSpec) {
108
129
  */
109
130
  async function resolveFirstRunModel({ agentDir, workspace }, options = {}) {
110
131
  const { config, path: configPath } = await loadConfig(agentDir).catch(failStartup);
111
- if (resolveModelSpec(options.model, config))
112
- return; // already set (flag > FASTAGENT_MODEL > config)
132
+ const configured = resolveModelSpec(options.model, config);
133
+ if (configured)
134
+ return configured; // already set (flag > FASTAGENT_MODEL > config)
113
135
  if (options.input === false)
114
- return; // --no-input: never prompt (clig) — the opener raises the clear error
136
+ return undefined; // --no-input: never prompt (clig) — the caller decides what's missing
115
137
  if (!isInteractive())
116
- return; // CI/deploy: the opener throws the actionable missing-model error
138
+ return undefined; // CI/deploy: the caller raises the actionable missing-model error
117
139
  const environment = agentModels(agentDir, {}, { cwd: workspace });
118
140
  // The picker lists the AGENT's surface: built-ins plus whatever its models.json declares, so a self-hosted endpoint
119
141
  // is pickable on first run instead of being invisible until hand-set.
120
142
  const models = await environment.runtime().catch(failStartup);
121
143
  const chosen = await pickWithCredentials(models, environment.auth.path, agentDir);
122
144
  if (chosen === undefined)
123
- return; // cancelled (or auth probe failed): the caller raises its clear missing-model error
145
+ return undefined; // cancelled (or auth probe failed): the caller raises its clear error
124
146
  process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
125
147
  await persistModelChoice(agentDir, configPath, chosen);
148
+ return chosen;
126
149
  }
127
150
  /** The credential-aware pick: full catalog annotated per provider, then the post-pick auth policy. */
128
151
  async function pickWithCredentials(models, authPath, agentDir) {
package/dist/core.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type { Agent, AgentEvent, ImageRef, Json, Prompt, Scope } from "./agent.ts";
2
- export { ABORTED_CODE, SESSION_BUSY_CODE } from "./agent.ts";
2
+ export { ABORTED_CODE, MISSING_MODEL_CODE, SESSION_BUSY_CODE } from "./agent.ts";
3
3
  export { collect, AgentFailure, type CollectResult } from "./collect.ts";
4
4
  export type { ModuleLoadFailure } from "./loader.ts";
5
5
  export { createInvokeHandler } from "./channels/http.ts";
package/dist/core.js CHANGED
@@ -1,7 +1,8 @@
1
- // VALUES, not types: each caller does something DIFFERENT with these two codes (retry with backoff, skip the
2
- // occurrence, steer the live run instead of failing), which is why they are named at all. Without an export an
3
- // embedder copies the literal, and the copy is how the first-event rule goes silently wrong.
4
- export { ABORTED_CODE, SESSION_BUSY_CODE } from "./agent.js";
1
+ // VALUES, not types: each caller does something DIFFERENT with these codes (busy: retry with backoff, skip the
2
+ // occurrence, or steer the live run instead of failing; aborted: settle a deliberate stop, never replay it;
3
+ // missing_model: do not retry, give the session a model or the agent a default), which is why they are named at all.
4
+ // Without an export an embedder copies the literal, and the copy is how the first-event rule goes silently wrong.
5
+ export { ABORTED_CODE, MISSING_MODEL_CODE, SESSION_BUSY_CODE } from "./agent.js";
5
6
  export { collect, AgentFailure } from "./collect.js";
6
7
  export { createInvokeHandler } from "./channels/http.js";
7
8
  export { ControlRequestError, connectAgent, connectSessionControl, } from "./session-remote.js";
@@ -116,7 +116,7 @@ export async function checkKeptIgnoreFiles(ctx, report) {
116
116
  // Asked as a DIRECTORY (trailing slash), which is what it is.
117
117
  if (excluded(`${basename(agentDir)}/`)) {
118
118
  const text = `your ${rel} (kept) excludes \`${basename(agentDir)}\` — the build context would ship WITHOUT the ` +
119
- `agent entirely (the deployed box has no persona/config and crash-loops). Remove that rule ` +
119
+ `agent entirely (the deployed box has no prompt/config and crash-loops). Remove that rule ` +
120
120
  `before deploying.`;
121
121
  report.issue(text);
122
122
  }
@@ -13,6 +13,7 @@ import { AGENT_MODEL_CATALOG_FILE, AGENT_MODELS_FILE, exists } from "../paths.js
13
13
  import { inspectChannels } from "../channels/discover.js";
14
14
  import { loadRoutines } from "../schedule/discover.js";
15
15
  import { resolveAgentTools } from "../engines/pi/create.js";
16
+ import { loadAgentDefinition } from "../engines/pi/definition.js";
16
17
  import { agentModels } from "../engines/pi/agent-models.js";
17
18
  import { allSecrets } from "../declared-secrets.js";
18
19
  import { createPiModelRuntime, definitionKeyOf, environmentAuthSource, literalKeyProviders, isBuiltinProvider, machineModels, globalCatalogPath, inGlobalCatalog, interactiveLoginKind, loginProviders, } from "../engines/pi/models.js";
@@ -64,6 +65,13 @@ async function gatherFacts(input, report) {
64
65
  `makes it an agent, never its name)`);
65
66
  }
66
67
  const agentPrefix = `${basename(agentDir)}/`;
68
+ // The definition the box will load on every start, loaded here first: a refusal in it (a leftover persona.md, a
69
+ // skill named with a slash) builds a perfectly good image that crash-loops on fly/railway/docker and fails every
70
+ // invocation on AgentCore. This is the boundary that turns that refusal into a deploy finding, so the error is
71
+ // carried whole into the issue rather than rethrown.
72
+ await loadAgentDefinition(agentDir, { cwd: workspace }).catch((error) => {
73
+ report.issue(`the definition does not load, so the deployed agent would not start: ${error instanceof Error ? error.message : String(error)}`);
74
+ });
67
75
  // The model this deployment will run on, and where it came from. Resolved HERE so the plan side and the run side
68
76
  // cannot disagree about it.
69
77
  const valueFile = relative(workspace, dotEnvPath(agentDir));
@@ -232,7 +240,7 @@ async function gatherFacts(input, report) {
232
240
  }
233
241
  const paths = await buildContextPaths(workspace, agentDir, agentPrefix, authPath);
234
242
  await checkKeptIgnoreFiles({ workspace, agentDir, agentPrefix, force, paths }, report);
235
- // Write-back mechanics are fastagent's (the policy is the persona's).
243
+ // Write-back mechanics are fastagent's (the policy is the agent's prompt's).
236
244
  const apt = shipsGit ? [...new Set(["git", ...(config.deploy?.apt ?? [])])] : config.deploy?.apt;
237
245
  const container = {
238
246
  releaseId: randomUUID(),
@@ -119,7 +119,7 @@ export async function runDevSupervisor(placement, options = {}) {
119
119
  timer = setTimeout(triggerReload, 200);
120
120
  });
121
121
  watcher.on("error", (error) => log.warn(`[fastagent] file watching error (${error.message}); some edits may need a manual restart`));
122
- log.info(`[fastagent] watching ${WATCHED_HINT} — code edits restart the dev worker (--no-watch to disable); AGENTS.md/persona.md/skills edits go live next turn without a restart`);
122
+ log.info(`[fastagent] watching ${WATCHED_HINT} — code edits restart the dev worker (--no-watch to disable); AGENTS.md/SYSTEM.md/APPEND_SYSTEM.md/skills/prompts edits go live next turn without a restart`);
123
123
  // FASTAGENT_SECRETS_DIR can move the `.env` OUT of the agent dir entirely.
124
124
  if (!isUnderDir(dotEnvPath(placement.agentDir), placement.agentDir)) {
125
125
  log.warn(`[fastagent] .env lives outside the agent dir (FASTAGENT_SECRETS_DIR → ${dotEnvPath(placement.agentDir)}) — it is NOT watched; restart dev after editing it`);
@@ -5,9 +5,23 @@ import type { PiAgentSessionFactory } from "./invoke-session.ts";
5
5
  import type { PiSessionRecordStore } from "./session-store.ts";
6
6
  import type { MountedTool } from "./tool.ts";
7
7
  import { type AnyModel } from "./models.ts";
8
- interface PiSessionDefinition {
8
+ import type { DefinitionPrompt } from "./definition.ts";
9
+ /** What one binding reads from the definition: the prompt pieces, skills and prompt templates pi is handed. */
10
+ export interface PiSessionDefinition {
11
+ /** Replaces pi's default prompt when set (`SYSTEM.md`, L2's `base`, L1's `instructions`); absent → pi's default. */
9
12
  systemPrompt?: string;
13
+ /** Added after the prompt, whichever it is (`APPEND_SYSTEM.md`). */
14
+ appendSystemPrompt?: string;
15
+ /** Project instructions (AGENTS.md); pi renders them as its project context. */
16
+ contextFiles?: Array<{
17
+ path: string;
18
+ content: string;
19
+ }>;
20
+ /** FastAgent's own named prompt sections: pi renders them after its own, whoever wrote the rest. */
21
+ sections?: Record<string, string>;
10
22
  skills: Skill[];
23
+ /** The definition's own prompt templates; the machine's are added behind them. */
24
+ prompts?: DefinitionPrompt[];
11
25
  }
12
26
  export interface PiAgentSessionFactoryOptions {
13
27
  /** Where conversations live. */
@@ -16,8 +30,11 @@ export interface PiAgentSessionFactoryOptions {
16
30
  engine: () => Promise<{
17
31
  modelRuntime: ModelRuntime;
18
32
  }>;
19
- /** Resolved after extensions register their models. */
20
- modelSpec: string;
33
+ /**
34
+ * The default model, resolved after extensions register their models. Optional: a session that records a model
35
+ * runs on it, and one that records none is refused with {@link MissingModel} when there is no default either.
36
+ */
37
+ modelSpec?: string;
21
38
  thinkingLevel?: ThinkingLevel;
22
39
  tools?: MountedTool[];
23
40
  /** Read once per binding; prompt and skills come from the same definition read. */
@@ -52,8 +69,7 @@ export declare function bindPiSession(options: BindPiSessionOptions): ReturnType
52
69
  type DefinitionLoaderOptions = NonNullable<CreateAgentSessionServicesOptions["resourceLoaderOptions"]>;
53
70
  /** The resource posture a fastagent definition asks pi for — ONE definition of it, for both assemblies. */
54
71
  export declare function definitionResourceLoaderOptions(source: {
55
- systemPrompt: () => string | undefined;
56
- skills: () => Skill[];
72
+ definition: PiSessionDefinition;
57
73
  /** {@link readMachine} for this workspace — resolved by the caller, because this function is synchronous. */
58
74
  machine: Machine;
59
75
  extensionPaths?: readonly string[];