@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
@@ -17,7 +17,7 @@ export function isPlaneMarker(entry) {
17
17
  * by "a message".
18
18
  *
19
19
  * The line it draws is the ENGINE's prompt state. Since pi 0.86 pi writes `system` messages into the same log the
20
- * conversation lives in, carrying the assembled prompt (persona, project context, skill and tool descriptions)
20
+ * conversation lives in, carrying the assembled prompt (system prompt, project context, skill and tool descriptions)
21
21
  * plus one more per prompt or tool-set change. They are bookkeeping, not something anyone said, and every reader
22
22
  * of the journal has to decide about them. Deciding once, here, is the point: the first reader that forgot cut
23
23
  * inheritance above every exchange and handed a new thread an empty history, with no diagnostic.
@@ -47,10 +47,21 @@ export interface SessionSettings {
47
47
  };
48
48
  };
49
49
  }
50
- /** `defaults` is the assembly's configured pair — what a session with no overrides runs on. */
50
+ /**
51
+ * `defaults` is the assembly's configured pair — what a session with no overrides runs on. An agent may set no default
52
+ * model, so a session runs on the model it records, else the default, else none: undefined then, and the session
53
+ * cannot run until it is given one ({@link MissingModel}).
54
+ */
51
55
  export declare function resolveSessionSettings(entries: OverrideEntryLike[], models: Models, defaults: {
52
- model: AnyModel;
56
+ model?: AnyModel;
53
57
  thinkingLevel: ThinkingLevel;
54
- }): SessionSettings;
58
+ }): SessionSettings | undefined;
59
+ /**
60
+ * A turn has no model to run on: its session records none this registry knows, and the agent sets no default. Its own
61
+ * class so the invoke answers it with `missing_model` instead of a prose failure.
62
+ */
63
+ export declare class MissingModel extends Error {
64
+ constructor(session: string, entries: OverrideEntryLike[]);
65
+ }
55
66
  /** The entries on the session's ACTIVE path, root→leaf — what every last-wins settings read walks. */
56
67
  export declare function activePath(record: SessionManager, from?: string): OverrideEntryLike[];
@@ -51,7 +51,11 @@ export function lastOverrideEntries(entries) {
51
51
  }
52
52
  return { model, thinkingLevel };
53
53
  }
54
- /** `defaults` is the assembly's configured pair — what a session with no overrides runs on. */
54
+ /**
55
+ * `defaults` is the assembly's configured pair — what a session with no overrides runs on. An agent may set no default
56
+ * model, so a session runs on the model it records, else the default, else none: undefined then, and the session
57
+ * cannot run until it is given one ({@link MissingModel}).
58
+ */
55
59
  export function resolveSessionSettings(entries, models, defaults) {
56
60
  const recorded = lastOverrideEntries(entries);
57
61
  const dropped = {};
@@ -64,6 +68,8 @@ export function resolveSessionSettings(entries, models, defaults) {
64
68
  else
65
69
  dropped.model = `${recorded.model.provider}/${recorded.model.modelId}`;
66
70
  }
71
+ if (!model)
72
+ return undefined;
67
73
  const availableThinkingLevels = getSupportedThinkingLevels(model);
68
74
  let thinkingLevel = clampThinkingLevel(model, defaults.thinkingLevel);
69
75
  if (recorded.thinkingLevel !== undefined) {
@@ -87,6 +93,19 @@ export function resolveSessionSettings(entries, models, defaults) {
87
93
  ...(dropped.model || dropped.thinkingLevel ? { dropped } : {}),
88
94
  };
89
95
  }
96
+ /**
97
+ * A turn has no model to run on: its session records none this registry knows, and the agent sets no default. Its own
98
+ * class so the invoke answers it with `missing_model` instead of a prose failure.
99
+ */
100
+ export class MissingModel extends Error {
101
+ constructor(session, entries) {
102
+ const recorded = lastOverrideEntries(entries).model;
103
+ super(`session "${session}" has no model: ${recorded
104
+ ? `the model it records, ${recorded.provider}/${recorded.modelId}, is not one this agent knows,`
105
+ : "it records none,"} and the agent sets no default — give the session one (session control's update({ model })), or the agent ` +
106
+ `a default ("model" in fastagent.config.ts, or FASTAGENT_MODEL)`);
107
+ }
108
+ }
90
109
  /** The entries on the session's ACTIVE path, root→leaf — what every last-wins settings read walks. */
91
110
  export function activePath(record, from) {
92
111
  // `from` asks a different question: the path a leaf move is ABOUT to make active, which a caller validating a patch
@@ -1,4 +1,4 @@
1
- import { type AssistantMessage, type ImageContent } from "@earendil-works/pi-ai";
1
+ import { type AssistantMessage, type ImageContent, type ToolResultMessage } from "@earendil-works/pi-ai";
2
2
  import { type AgentEvent, type Prompt } from "../../agent.ts";
3
3
  import type { AnswerOutcome, PromptDisposition, SessionEvent } from "../../session.ts";
4
4
  export type Release = () => void;
@@ -18,6 +18,36 @@ export declare function classifyRetryable(details: string, signal: {
18
18
  * the recorded one cannot disagree.
19
19
  */
20
20
  export declare function answerOutcome(message: AssistantMessage): AnswerOutcome | undefined;
21
+ /**
22
+ * An answer's recorded thinking: its published thinking blocks, in order, joined with nothing between them, exactly as
23
+ * their live deltas add up (the stream marks no block boundary, and the answer's `text` is joined the same way).
24
+ * Undefined when it has none.
25
+ */
26
+ export declare function answerThinking(message: AssistantMessage): string | undefined;
27
+ /** Whether a live thinking delta is streamed: the block it extends is one {@link answerThinking} will publish. */
28
+ export declare function streamsThinkingDelta(event: {
29
+ contentIndex: number;
30
+ partial: AssistantMessage;
31
+ }): boolean;
32
+ /**
33
+ * Whether a finished tool call asked to end its run (pi's `terminate`: returned by the tool, or set by a `tool_call`
34
+ * hook that blocks it; pi 0.99.2's `tool_result` hook cannot set it). After a batch whose EVERY result asks it, pi
35
+ * makes no further model call for that batch, so unless a queued steer or follow-up continues the run, it completes on
36
+ * `tool` entries with no answer after them: the same shape a process killed after its tools leaves. A call made from
37
+ * inside another tool (`parentToolCallId`, a codemode script) is not in the batch and does not count.
38
+ * The ONE reading: the live `tool_finished` and the flag {@link markEndsRun} records both come from it.
39
+ */
40
+ export declare function asksToEndRun(event: {
41
+ result?: unknown;
42
+ parentToolCallId?: string;
43
+ }): boolean;
44
+ /**
45
+ * The tool result message as pi then records it, carrying the request: pi's `createToolResultMessage` drops
46
+ * `terminate` (as of pi 0.99.2), so without this nothing in the record says the run ended on purpose.
47
+ */
48
+ export declare function markEndsRun(message: ToolResultMessage): ToolResultMessage;
49
+ /** Whether a recorded tool result carries {@link markEndsRun}'s flag. */
50
+ export declare function endsRun(message: ToolResultMessage): boolean;
21
51
  /**
22
52
  * Terminal mapping, decided by the resolved message's outcome: pi's `prompt()` RESOLVES a message with stopReason
23
53
  * "error"/"aborted" rather than throwing, so relying on catch alone would miss that whole failure class and violate
@@ -1,6 +1,6 @@
1
1
  /** The turn mechanism's ENGINE-agnostic half: the parts that describe a turn rather than pi. */
2
2
  import { stripVTControlCharacters } from "node:util";
3
- import { contentText } from "@earendil-works/pi-ai";
3
+ import { contentText, } from "@earendil-works/pi-ai";
4
4
  import { ABORTED_CODE } from "../../agent.js";
5
5
  import { log } from "../../log.js";
6
6
  export function inProcessLease() {
@@ -91,6 +91,59 @@ export function answerOutcome(message) {
91
91
  return undefined;
92
92
  }
93
93
  }
94
+ /**
95
+ * Whether a thinking block is PUBLISHED: readable reasoning. A redacted one holds a provider's opaque payload and a
96
+ * placeholder (pi writes "[Reasoning redacted]" for it), not reasoning anyone can read, so it is neither streamed nor
97
+ * read back. The ONE rule: the live `message_delta { channel: "thinking" }` ({@link streamsThinkingDelta}) and the
98
+ * `assistant` entry's `thinking` ({@link answerThinking}) both apply it, so a client reads the same text either way.
99
+ *
100
+ * One exception, by when the flag is read: live reads it as each delta arrives, the record as the answer ended. pi's
101
+ * Bedrock path flags a block redacted when its first encrypted chunk arrives, and its readable text and its encrypted
102
+ * chunks are separate fields, so a block that streamed readable text BEFORE turning redacted had that text streamed
103
+ * and is then not read back. Not separable after the fact: the record keeps one flag and one string per block, with
104
+ * pi's placeholder appended. Whether a Bedrock model sends both in one block is unverified.
105
+ */
106
+ function isPublishedThinking(block) {
107
+ return block?.type === "thinking" && block.redacted !== true;
108
+ }
109
+ /**
110
+ * An answer's recorded thinking: its published thinking blocks, in order, joined with nothing between them, exactly as
111
+ * their live deltas add up (the stream marks no block boundary, and the answer's `text` is joined the same way).
112
+ * Undefined when it has none.
113
+ */
114
+ export function answerThinking(message) {
115
+ const text = message.content
116
+ .filter(isPublishedThinking)
117
+ .map((block) => block.thinking)
118
+ .join("");
119
+ return text === "" ? undefined : text;
120
+ }
121
+ /** Whether a live thinking delta is streamed: the block it extends is one {@link answerThinking} will publish. */
122
+ export function streamsThinkingDelta(event) {
123
+ return isPublishedThinking(event.partial.content[event.contentIndex]);
124
+ }
125
+ /**
126
+ * Whether a finished tool call asked to end its run (pi's `terminate`: returned by the tool, or set by a `tool_call`
127
+ * hook that blocks it; pi 0.99.2's `tool_result` hook cannot set it). After a batch whose EVERY result asks it, pi
128
+ * makes no further model call for that batch, so unless a queued steer or follow-up continues the run, it completes on
129
+ * `tool` entries with no answer after them: the same shape a process killed after its tools leaves. A call made from
130
+ * inside another tool (`parentToolCallId`, a codemode script) is not in the batch and does not count.
131
+ * The ONE reading: the live `tool_finished` and the flag {@link markEndsRun} records both come from it.
132
+ */
133
+ export function asksToEndRun(event) {
134
+ return !event.parentToolCallId && event.result?.terminate === true;
135
+ }
136
+ /**
137
+ * The tool result message as pi then records it, carrying the request: pi's `createToolResultMessage` drops
138
+ * `terminate` (as of pi 0.99.2), so without this nothing in the record says the run ended on purpose.
139
+ */
140
+ export function markEndsRun(message) {
141
+ return { ...message, terminate: true };
142
+ }
143
+ /** Whether a recorded tool result carries {@link markEndsRun}'s flag. */
144
+ export function endsRun(message) {
145
+ return message.terminate === true;
146
+ }
94
147
  /**
95
148
  * Terminal mapping, decided by the resolved message's outcome: pi's `prompt()` RESOLVES a message with stopReason
96
149
  * "error"/"aborted" rather than throwing, so relying on catch alone would miss that whole failure class and violate
package/dist/paths.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare const AGENT_MODELS_FILE = "models.json";
23
23
  export declare const AGENT_MODEL_CATALOG_FILE = "models-store.json";
24
24
  export interface ResolvedPlacement {
25
25
  /**
26
- * The AGENT directory — where the definition (persona.md/skills/tools/channels/routines), the config, and the
26
+ * The AGENT directory — where the definition (SYSTEM.md/skills/tools/channels/routines), the config, and the
27
27
  * machinery dirs (`.secrets/`, `.state/`) live.
28
28
  */
29
29
  agentDir: string;
package/dist/paths.js CHANGED
@@ -35,7 +35,18 @@ export const AGENT_MODEL_CATALOG_FILE = "models-store.json";
35
35
  * The definition paths an agent LOADS content from — the surface a second agent must not be scaffolded inside
36
36
  * ({@link agentDefinitionOwner}), because the outer agent would read it as its own skills/tools.
37
37
  */
38
- const LOADED_SURFACE = ["persona.md", "skills", "tools", "channels", "routines"];
38
+ const LOADED_SURFACE = [
39
+ "SYSTEM.md",
40
+ "APPEND_SYSTEM.md",
41
+ "skills",
42
+ "prompts",
43
+ "tools",
44
+ "channels",
45
+ "routines",
46
+ // pi's and the standard spellings the definition also reads: `.pi/skills`, `.pi/prompts`, `.agents/skills`.
47
+ ".pi",
48
+ ".agents",
49
+ ];
39
50
  /**
40
51
  * "Not there" and "could not look" are different answers, and only the first may read as an absence: an agent
41
52
  * directory the caller cannot enter (EACCES) reported as no-agent-here ends in `run \`fastagent init\` to
package/dist/pi.d.ts CHANGED
@@ -6,7 +6,7 @@ export type { AgentTool, ExecutionEnv, Skill, SkillDiagnostic } from "@earendil-
6
6
  /** A conversation record, as the tool runtime and the control plane hold it. */
7
7
  export type { SessionManager, SessionEntry as PiSessionEntry } from "@earendil-works/pi-coding-agent";
8
8
  export { availableModelsFromDir, createPiAgentFromDir, type CreatePiAgentFromDirOptions, refreshMachineModelCatalog, refreshModelCatalog, } from "./engines/pi/open.ts";
9
- export type { LoadedDefinition, SkillCollision } from "./engines/pi/definition.ts";
9
+ export type { DefinitionDiagnostic, DefinitionFile, DefinitionPrompt, DefinitionShadow, LoadedDefinition, SkillCollision, } from "./engines/pi/definition.ts";
10
10
  export { defineConfig, listModels, resolveModel, type FastagentConfig, } from "./engines/pi/config.ts";
11
11
  export type { SessionObserver } from "./engines/pi/turn-kit.ts";
12
12
  export { inProcessLease, type Lease, type Release } from "./engines/pi/turn-kit.ts";
@@ -35,8 +35,8 @@ export async function scaffoldAgent(dir, options = {}) {
35
35
  content: baseTemplate(`skills/writing-great-skills/${name}`),
36
36
  });
37
37
  const files = [
38
- // ① identity.
39
- { rel: join(root, "persona.md"), content: baseTemplate("persona.md") },
38
+ // Standing instructions, added to pi's default prompt (SYSTEM.md would replace it).
39
+ { rel: join(root, "APPEND_SYSTEM.md"), content: baseTemplate("APPEND_SYSTEM.md") },
40
40
  // The example skill: how to author skills well — the core of self-iteration.
41
41
  skill("SKILL.md"),
42
42
  skill("GLOSSARY.md"),
@@ -0,0 +1,12 @@
1
+ # Standing instructions
2
+
3
+ These instructions are added to your system prompt on every turn. This file, `APPEND_SYSTEM.md`, is re-read every turn along with the rest of your definition (`skills/` — capabilities you load when a task calls for them; `prompts/` — prompt templates; `tools/` — code tools your author added, in the same directory as this file). An edit to any of them takes effect on your next message, no restart.
4
+
5
+ Your definition is this directory: `APPEND_SYSTEM.md`, `skills/`, `tools/`, and the config beside them. Your WORKSPACE is the directory you were started in — the project you work on. It may be this same directory, or the one containing it; `fastagent info` prints both. Use only the tools actually listed in your system prompt. If the workspace has an `AGENTS.md`, it is project context — follow it without assuming a file tool is available.
6
+
7
+ When your mounted tools allow it, you can improve yourself. When a task reveals something durable — a repeatable process, a standing preference, a hard-won fact — write it into your definition instead of losing it:
8
+
9
+ - A repeatable process or capability → a new skill beside this file: `skills/<name>/SKILL.md`. Read `skills/writing-great-skills/SKILL.md` first; it is the guide to authoring skills well.
10
+ - A standing instruction or fact → edit this file.
11
+
12
+ Keep both lean: include only what changes your behavior, and delete what no longer earns its place.
@@ -1,5 +1,5 @@
1
1
  // fastagent.config.ts — deployment choices only (model / http; code tools auto-discover from tools/).
2
- // Your agent's identity lives in persona.md; its capabilities in skills/ + tools/ — never here.
2
+ // Your agent's prompt lives in SYSTEM.md / APPEND_SYSTEM.md; its capabilities in skills/ + tools/ — never here.
3
3
  // An AGENTS.md in the WORKSPACE (the directory the agent is started in) is read as project context.
4
4
  // Model precedence: `--model` flag > FASTAGENT_MODEL env > this default.
5
5
  // No model is preset: `fastagent dev` shows the full model catalog (models you already have
package/dist/session.d.ts CHANGED
@@ -347,6 +347,10 @@ export type UserMessageEvent = SessionEvent<"user_message", {
347
347
  }> & {
348
348
  runId: string;
349
349
  };
350
+ /**
351
+ * A piece of the answer being written. The `thinking` deltas of one answer add up to its `assistant` entry's
352
+ * `thinking`, as its `text` deltas add up to its `text`; redacted reasoning is streamed in neither.
353
+ */
350
354
  export type MessageDeltaEvent = SessionEvent<"message_delta", {
351
355
  channel: "text" | "thinking";
352
356
  delta: string;
@@ -388,11 +392,17 @@ export type ToolProgressEvent = SessionEvent<"tool_progress", {
388
392
  }> & {
389
393
  runId: string;
390
394
  };
395
+ /**
396
+ * `terminate: true` when the call asked to end its run. Once every call answering one assistant message asks it, the
397
+ * model is not called again for them, and the run ends there with no answer after its `tool` entries (which carry the
398
+ * same flag) unless a queued steer or follow-up continues it. Whether the run ended is `run_settled`'s to say.
399
+ */
391
400
  export type ToolFinishedEvent = SessionEvent<"tool_finished", {
392
401
  id: string;
393
402
  isError: boolean;
394
403
  content: Json;
395
404
  parentToolCallId?: string;
405
+ terminate?: true;
396
406
  }> & {
397
407
  runId: string;
398
408
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastagent-sh/fastagent",
3
- "version": "0.24.1",
3
+ "version": "0.24.3",
4
4
  "description": "Vibe first. Then FastAgent: turn a local agent directory into a live service in your app, on Telegram, Slack, Feishu, or behind any channel.",
5
5
  "keywords": [
6
6
  "agent",
@@ -1,12 +0,0 @@
1
- # Persona
2
-
3
- You are this workspace's agent. This file is your identity — it overrides the engine's default identity line, and it is re-read every turn along with the rest of your definition (`skills/` — capabilities you load when a task calls for them; `tools/` — code tools your author added, in the same directory as this file). An edit to any of them takes effect on your next message, no restart.
4
-
5
- Your definition is this directory: `persona.md`, `skills/`, `tools/`, and the config beside them. Your WORKSPACE is the directory you were started in — the project you work on. It may be this same directory, or the one containing it; `fastagent info` prints both. Use only the tools actually listed in your system prompt. If the workspace has an `AGENTS.md`, it is project context — follow it without assuming a file tool is available.
6
-
7
- When your mounted tools allow it, you can improve yourself. When a task reveals something durable — a repeatable process, a standing preference, a hard-won fact — write it into your definition instead of losing it:
8
-
9
- - A repeatable process or capability → a new skill beside this file: `skills/<name>/SKILL.md`. Only the `skills/` next to this file is scanned. Read `skills/writing-great-skills/SKILL.md` first; it is the guide to authoring skills well.
10
- - A standing instruction or fact → edit this file.
11
-
12
- Keep both lean: include only what changes your behavior, and delete what no longer earns its place.