@fastagent-sh/fastagent 0.18.0 → 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.
- package/README.md +25 -7
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +19 -0
- package/dist/atomic-write.js +48 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-service.d.ts +54 -0
- package/dist/channels/agentcore-service.js +111 -0
- package/dist/channels/agentcore.d.ts +12 -2
- package/dist/channels/agentcore.js +6 -4
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +16 -6
- package/dist/channels/control.js +106 -4
- package/dist/{engines/pi/channel.d.ts → channels/discover.d.ts} +2 -2
- package/dist/{engines/pi/channel.js → channels/discover.js} +8 -12
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.js +1 -1
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +7 -7
- package/dist/channels/feishu/invoke-turn.d.ts +1 -1
- package/dist/channels/feishu/invoke-turn.js +3 -4
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +1 -109
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +2 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/{state.js → kit/state.js} +4 -7
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +2 -2
- package/dist/channels/{tasks.js → kit/tasks.js} +1 -1
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.js → kit/turn-store.js} +1 -1
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +8 -5
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/slack-api.js +1 -1
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +8 -8
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/telegram/parse.js +2 -2
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/telegram.d.ts +1 -1
- package/dist/channels/telegram/telegram.js +3 -3
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/commands/add.js +1 -1
- package/dist/cli/commands/dev.js +25 -13
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +7 -4
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +63 -94
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +18 -57
- package/dist/cli/serve.js +57 -243
- package/dist/cli/shared.d.ts +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/preflight.js +1 -1
- package/dist/dev-supervisor.js +9 -3
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +314 -0
- package/dist/engines/pi/config.d.ts +4 -4
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +56 -43
- package/dist/engines/pi/create.js +141 -71
- package/dist/engines/pi/definition.d.ts +41 -7
- package/dist/engines/pi/definition.js +115 -11
- package/dist/engines/pi/invoke-session.d.ts +22 -19
- package/dist/engines/pi/invoke-session.js +286 -100
- package/dist/engines/pi/login.js +7 -4
- package/dist/engines/pi/models.d.ts +14 -0
- package/dist/engines/pi/models.js +9 -1
- package/dist/engines/pi/open.d.ts +9 -6
- package/dist/engines/pi/open.js +20 -10
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +19 -0
- package/dist/engines/pi/session-builder.js +84 -88
- package/dist/engines/pi/session-control.d.ts +10 -9
- package/dist/engines/pi/session-control.js +114 -77
- package/dist/engines/pi/session-inheritance.d.ts +44 -0
- package/dist/engines/pi/session-inheritance.js +242 -0
- package/dist/engines/pi/session-settings.d.ts +14 -2
- package/dist/engines/pi/session-settings.js +18 -0
- package/dist/engines/pi/session-store.d.ts +70 -0
- package/dist/engines/pi/session-store.js +313 -0
- package/dist/engines/pi/tool-context.d.ts +18 -8
- package/dist/engines/pi/tool-context.js +23 -1
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +1 -1
- package/dist/engines/pi/turn-kit.d.ts +32 -6
- package/dist/engines/pi/turn-kit.js +28 -1
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +10 -0
- package/dist/pi.d.ts +14 -8
- package/dist/pi.js +6 -5
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +1 -1
- package/dist/service.d.ts +158 -0
- package/dist/service.js +354 -0
- package/dist/session-remote.d.ts +1 -1
- package/dist/session.d.ts +4 -2
- package/dist/session.js +1 -1
- package/package.json +14 -5
- package/dist/engines/pi/harness.d.ts +0 -115
- package/dist/engines/pi/harness.js +0 -154
- package/dist/engines/pi/invoke.d.ts +0 -60
- package/dist/engines/pi/invoke.js +0 -459
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -60
- package/dist/engines/pi/sessions.js +0 -464
- package/dist/host/node.js +0 -52
- /package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +0 -0
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
- /package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
- /package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session persistence for the `AgentSession` L0 — open-or-create a durable record by the Caller's
|
|
3
|
+
* opaque session id, on pi-coding-agent's `SessionManager` (the v3 jsonl every pi surface reads).
|
|
4
|
+
*
|
|
5
|
+
* Records written before this store existed (by the pi-agent-core `Session` the serving path used
|
|
6
|
+
* to run on) are the same v3 jsonl and are continued in place — see `legacySessionId`. That is a
|
|
7
|
+
* READ path for existing conversations, not a second engine.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
11
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { log } from "../../log.js";
|
|
13
|
+
import { copyBranchInto, forkForInheritance, inheritanceCut } from "./session-inheritance.js";
|
|
14
|
+
/**
|
|
15
|
+
* A Caller's session id, as a name pi will accept.
|
|
16
|
+
*
|
|
17
|
+
* `SessionManager` enforces `^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$` — which no built-in
|
|
18
|
+
* channel satisfies: telegram's `-1001234567890` leads with a dash, feishu and slack keys carry
|
|
19
|
+
* `:` and `/`, and any custom `route()` may mint anything at all.
|
|
20
|
+
*
|
|
21
|
+
* The mapping is INJECTIVE, because two conversations resolving to one record is two rooms sharing a
|
|
22
|
+
* memory. `s` prefix (the pattern demands an alphanumeric head, and prefixing unconditionally is what
|
|
23
|
+
* keeps it injective — a conditional one would map `-a` and `s-a` alike), then each character
|
|
24
|
+
* outside `[A-Za-z0-9.-]` becomes `_XX` / `_uXXXX`, self-describing widths so no two inputs can
|
|
25
|
+
* produce one output. `_` escapes itself for the same reason. A trailing `.` or `-` is legal
|
|
26
|
+
* mid-name but not at the end, so it escapes too.
|
|
27
|
+
*
|
|
28
|
+
* Injective within this encoding — which is only sufficient because new records live in their own
|
|
29
|
+
* directory. The older spelling draws names from the same character set (it stored a room literally
|
|
30
|
+
* called `s42` as `s42`, which is also this encoding of `42`), so one directory would make some
|
|
31
|
+
* names ambiguous no matter how either side spells them.
|
|
32
|
+
*
|
|
33
|
+
* Readability is deliberate: `-1001234567890` becomes `s-1001234567890`, so an operator can still
|
|
34
|
+
* tell which room a file belongs to.
|
|
35
|
+
*/
|
|
36
|
+
export function piSessionId(sessionId) {
|
|
37
|
+
const hex = (c) => {
|
|
38
|
+
const code = c.charCodeAt(0);
|
|
39
|
+
return code < 0x100
|
|
40
|
+
? `_${code.toString(16).toUpperCase().padStart(2, "0")}`
|
|
41
|
+
: `_u${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
42
|
+
};
|
|
43
|
+
const body = sessionId.replace(/[^A-Za-z0-9.-]/g, hex).replace(/[.-]$/, hex);
|
|
44
|
+
return `s${body}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Disk-backed store under `dir`: restart the process, conversations continue.
|
|
48
|
+
*
|
|
49
|
+
* Lookup is a directory scan (`SessionManager.list`) because pi names files `<timestamp>_<id>.jsonl`
|
|
50
|
+
* and the timestamp is not ours to predict — the same trade sessions.ts makes today.
|
|
51
|
+
*
|
|
52
|
+
* A record written before this store existed keeps ITS id (the older path spelled them differently),
|
|
53
|
+
* so the scan accepts either: a conversation that predates this store is continued rather than
|
|
54
|
+
* silently restarted as an empty one. Nothing is rewritten on disk.
|
|
55
|
+
*
|
|
56
|
+
* NEW records live in a subdirectory of their own, because the two engines cannot share a namespace:
|
|
57
|
+
* both spell ids into `[A-Za-z0-9._-]`, so neither can claim a prefix the other cannot produce, and
|
|
58
|
+
* a directory holding both would have names that belong to two conversations at once — in whichever
|
|
59
|
+
* direction it is read. Separate directories make each side's own injectivity sufficient.
|
|
60
|
+
*
|
|
61
|
+
* A PRE-EXISTING record is still continued in place: it is looked up by the older spelling, which is
|
|
62
|
+
* injective on its own terms, and appended to where it lies. Both spellings are the same v3 jsonl,
|
|
63
|
+
* so a conversation started before this store keeps going rather than restarting empty.
|
|
64
|
+
*
|
|
65
|
+
* SCOPE OF "open-or-create": idempotent against a store that is serialized per session, which is what
|
|
66
|
+
* the serving path provides — the single-writer lease is taken before any store call, so no two
|
|
67
|
+
* turns of one conversation reach this at once. What it does NOT do is arbitrate a FIRST open racing
|
|
68
|
+
* across processes: two instances that scan before either writes will both create, and the
|
|
69
|
+
* conversation forks into two records. sessions.ts states the same boundary for the same reason
|
|
70
|
+
* ("the serving path serializes it with the single-writer lease before reaching any store"), and a
|
|
71
|
+
* horizontally-scaled deployment that wants more owes a lease that spans its instances — an
|
|
72
|
+
* in-process one cannot arbitrate between them, and a file lock here would only look like it could.
|
|
73
|
+
*/
|
|
74
|
+
export function piSessionRecordStore(options) {
|
|
75
|
+
const cwd = options.cwd ?? process.cwd();
|
|
76
|
+
// Resolved against the workspace this store serves, not against wherever the process happens to
|
|
77
|
+
// have been started: a relative `dir` means "inside this agent", and a serving process may chdir.
|
|
78
|
+
const root = resolve(cwd, options.dir);
|
|
79
|
+
const own = join(root, OWN_RECORDS_DIR);
|
|
80
|
+
/** Where a forked record is finished before it becomes discoverable. A SUBDIRECTORY of the store,
|
|
81
|
+
* so `list()` (one level, `*.jsonl`) never sees a record that is still being prepared. */
|
|
82
|
+
const staging = join(own, ".staging");
|
|
83
|
+
/** Fork the named parent into `id`, or answer undefined so the caller starts empty. Every failure
|
|
84
|
+
* is a warn: a thread must not lose its first turn to an inheritance edge. */
|
|
85
|
+
const inheritInto = async (id, inherit) => {
|
|
86
|
+
const parentId = piSessionId(inherit.parentSession);
|
|
87
|
+
const found = (await SessionManager.list(cwd, own)).find((r) => r.id === parentId) ??
|
|
88
|
+
(await SessionManager.list(cwd, root)).find((r) => r.id === legacySessionId(inherit.parentSession));
|
|
89
|
+
if (!found) {
|
|
90
|
+
log.warn(`[fastagent] session "${id}" names parent "${inherit.parentSession}", which has no record — starting empty`);
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const parentDir = found.id === parentId ? own : root;
|
|
95
|
+
mkdirSync(staging, { recursive: true });
|
|
96
|
+
const staged = forkForInheritance({
|
|
97
|
+
// A parent that crashed mid tool-execution would otherwise pass its dangling tool_use down
|
|
98
|
+
// to the child, whose very first request the provider then rejects.
|
|
99
|
+
parent: reconcileInterruptedToolCalls(SessionManager.open(found.path, parentDir)),
|
|
100
|
+
id,
|
|
101
|
+
cwd,
|
|
102
|
+
stagingDir: staging,
|
|
103
|
+
branchHints: inherit.branchHints,
|
|
104
|
+
});
|
|
105
|
+
if (!staged)
|
|
106
|
+
return undefined;
|
|
107
|
+
// Publish only once the record is complete: same-filesystem rename, so a reader sees the whole
|
|
108
|
+
// thing or nothing at all.
|
|
109
|
+
const stagedFile = staged.getSessionFile();
|
|
110
|
+
if (!stagedFile)
|
|
111
|
+
return staged; // non-persisting backend: nothing to publish
|
|
112
|
+
const target = join(own, basename(stagedFile));
|
|
113
|
+
renameSync(stagedFile, target);
|
|
114
|
+
return SessionManager.open(target, own);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// Unattributed on purpose: this spans reading the parent AND writing the child.
|
|
118
|
+
log.warn(`[fastagent] could not inherit from "${inherit.parentSession}" into "${id}" (${String(error)}) — starting empty`);
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
async openOrCreate(sessionId, inherit) {
|
|
124
|
+
const id = piSessionId(sessionId);
|
|
125
|
+
const mine = (await SessionManager.list(cwd, own)).find((r) => r.id === id);
|
|
126
|
+
if (mine)
|
|
127
|
+
return reconcileInterruptedToolCalls(SessionManager.open(mine.path, own));
|
|
128
|
+
const legacy = (await SessionManager.list(cwd, root)).find((r) => r.id === legacySessionId(sessionId));
|
|
129
|
+
if (legacy)
|
|
130
|
+
return reconcileInterruptedToolCalls(SessionManager.open(legacy.path, root));
|
|
131
|
+
mkdirSync(own, { recursive: true });
|
|
132
|
+
// Inheritance is a CREATE-path decision: an existing session above ignores it entirely, which
|
|
133
|
+
// is what makes it one-time by construction.
|
|
134
|
+
if (inherit) {
|
|
135
|
+
const inherited = await inheritInto(id, inherit);
|
|
136
|
+
if (inherited)
|
|
137
|
+
return inherited;
|
|
138
|
+
}
|
|
139
|
+
return publish(SessionManager.create(cwd, own, { id }), own);
|
|
140
|
+
},
|
|
141
|
+
async openIfExists(sessionId) {
|
|
142
|
+
const id = piSessionId(sessionId);
|
|
143
|
+
const mine = (await SessionManager.list(cwd, own)).find((r) => r.id === id);
|
|
144
|
+
if (mine)
|
|
145
|
+
return SessionManager.open(mine.path, own);
|
|
146
|
+
const legacy = (await SessionManager.list(cwd, root)).find((r) => r.id === legacySessionId(sessionId));
|
|
147
|
+
return legacy ? SessionManager.open(legacy.path, root) : undefined;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Crash-safety reconciliation, run on every OPEN of an existing record.
|
|
153
|
+
*
|
|
154
|
+
* A turn that dies mid tool-execution leaves an assistant `tool_use` with no matching result (the
|
|
155
|
+
* assistant message is persisted before the tool runs). The next turn would then hand the provider
|
|
156
|
+
* an `assistant(tool_use) -> user` sequence that Anthropic and OpenAI reject — the session is
|
|
157
|
+
* poisoned. An honest "interrupted" error result is appended for each dangling call, restoring a
|
|
158
|
+
* valid transcript. Tool side-effect idempotency stays the tool's responsibility (SPEC §6); this
|
|
159
|
+
* restores transcript validity, not exactly-once execution.
|
|
160
|
+
*
|
|
161
|
+
* Pairing is TURN-LOCAL: a tool_use is paired only by a toolResult that immediately follows it (up
|
|
162
|
+
* to the next non-toolResult). Tool-call ids are not unique across turns — a local model may restart
|
|
163
|
+
* them each response — so matching against the whole transcript could falsely settle a leaf call
|
|
164
|
+
* against an earlier turn's identical id. An append-only log can only repair a gap AT THE LEAF; an
|
|
165
|
+
* earlier one is surfaced via log.warn rather than "fixed" with an orphaned result.
|
|
166
|
+
*
|
|
167
|
+
* The synthetic result splits its audiences: `content` (read by the model, may reach the end user)
|
|
168
|
+
* stays neutral — it must NOT say "aborted" (pi's word for a user cancellation) or leak infra
|
|
169
|
+
* detail; `details` carries the operational marker for developers and never reaches the provider.
|
|
170
|
+
*/
|
|
171
|
+
function reconcileInterruptedToolCalls(record) {
|
|
172
|
+
const messages = record.getBranch().flatMap((entry) => {
|
|
173
|
+
const message = entry.message;
|
|
174
|
+
return entry.type === "message" && message ? [message] : [];
|
|
175
|
+
});
|
|
176
|
+
let leafIdx = -1;
|
|
177
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
178
|
+
if (messages[i]?.role === "assistant") {
|
|
179
|
+
leafIdx = i;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (leafIdx === -1)
|
|
184
|
+
return record; // no assistant turn yet
|
|
185
|
+
const leafReparable = messages.slice(leafIdx + 1).every((m) => m.role === "toolResult");
|
|
186
|
+
const toRepair = [];
|
|
187
|
+
const orphaned = [];
|
|
188
|
+
messages.forEach((m, idx) => {
|
|
189
|
+
if (m.role !== "assistant")
|
|
190
|
+
return;
|
|
191
|
+
const paired = new Set();
|
|
192
|
+
for (let j = idx + 1; j < messages.length; j++) {
|
|
193
|
+
const next = messages[j];
|
|
194
|
+
if (next?.role !== "toolResult")
|
|
195
|
+
break;
|
|
196
|
+
paired.add(next.toolCallId);
|
|
197
|
+
}
|
|
198
|
+
for (const block of m.content) {
|
|
199
|
+
if (block.type !== "toolCall" || paired.has(block.id))
|
|
200
|
+
continue;
|
|
201
|
+
if (idx === leafIdx && leafReparable)
|
|
202
|
+
toRepair.push({ id: block.id, name: block.name });
|
|
203
|
+
else
|
|
204
|
+
orphaned.push(block.id);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
if (orphaned.length > 0) {
|
|
208
|
+
log.warn(`[fastagent] unmatched tool_use is not at the session leaf; leaving it unreconciled ` +
|
|
209
|
+
`(an append-only log cannot repair a mid-history gap): toolCallIds=${orphaned.join(",")}`);
|
|
210
|
+
}
|
|
211
|
+
for (const { id, name } of toRepair) {
|
|
212
|
+
record.appendMessage({
|
|
213
|
+
role: "toolResult",
|
|
214
|
+
toolCallId: id,
|
|
215
|
+
toolName: name,
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: "text",
|
|
219
|
+
text: "This tool call did not complete and its result is unavailable. Re-run it if the result is still needed.",
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
details: { fastagent: "interrupted-tool-call" },
|
|
223
|
+
isError: true,
|
|
224
|
+
timestamp: Date.now(),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return record;
|
|
228
|
+
}
|
|
229
|
+
/** Where this engine's own records live, under the sessions directory both engines are pointed at. */
|
|
230
|
+
const OWN_RECORDS_DIR = "agent-session";
|
|
231
|
+
/**
|
|
232
|
+
* Make a NEW record exist on disk before anyone can act on it.
|
|
233
|
+
*
|
|
234
|
+
* `SessionManager` buffers a new session in memory and writes nothing until the first ASSISTANT
|
|
235
|
+
* message arrives (`_persist` returns early while no assistant entry exists). Two consequences, and
|
|
236
|
+
* the second is why this cannot be left to the engine:
|
|
237
|
+
*
|
|
238
|
+
* - a crash between "the user asked" and "the model answered" loses the question, while the record
|
|
239
|
+
* pi-agent-core's storage wrote it immediately (conformance-levels.md §5 named this gap);
|
|
240
|
+
* - **open-or-create stops being idempotent**: the second call cannot find the first call's record,
|
|
241
|
+
* so one conversation forks into two files, each with half the history.
|
|
242
|
+
*
|
|
243
|
+
* Writing pi's OWN header (`getHeader()`, not a hand-built literal) and reopening puts the manager
|
|
244
|
+
* on its normal "file exists" path, where every append lands immediately.
|
|
245
|
+
*/
|
|
246
|
+
function publish(session, dir) {
|
|
247
|
+
const file = session.getSessionFile();
|
|
248
|
+
if (!file || existsSync(file))
|
|
249
|
+
return session; // in-memory, or already on disk
|
|
250
|
+
const header = session.getHeader();
|
|
251
|
+
if (!header)
|
|
252
|
+
return session;
|
|
253
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
254
|
+
writeFileSync(file, `${JSON.stringify(header)}\n`, { flag: "wx" });
|
|
255
|
+
return SessionManager.open(file, dir);
|
|
256
|
+
}
|
|
257
|
+
/** In-process store: continuity lives and dies with the instance. */
|
|
258
|
+
export function piInMemorySessionRecordStore(options = {}) {
|
|
259
|
+
const cwd = options.cwd ?? process.cwd();
|
|
260
|
+
const live = new Map();
|
|
261
|
+
return {
|
|
262
|
+
async openOrCreate(sessionId, inherit) {
|
|
263
|
+
// Keyed by the CALLER's id: the encoding exists to satisfy pi's filename rule, and in memory
|
|
264
|
+
// there are no filenames — two rooms whose encodings collide must still not share a map slot.
|
|
265
|
+
const existing = live.get(sessionId);
|
|
266
|
+
if (existing)
|
|
267
|
+
return reconcileInterruptedToolCalls(existing);
|
|
268
|
+
const fresh = () => SessionManager.inMemory(cwd, { id: piSessionId(sessionId) });
|
|
269
|
+
// Same semantics as the durable store, different mechanism: with no file to fork, the parent's
|
|
270
|
+
// path is copied entry by entry. Inheritance is a property of the CONTRACT, not of the medium —
|
|
271
|
+
// a caller must not get a thread that forgot its room because the store is in memory.
|
|
272
|
+
//
|
|
273
|
+
// And the same atomicity: the session is REGISTERED only once it is complete. Registering
|
|
274
|
+
// first and copying after would leave a half-inherited thread in place on any failure, while
|
|
275
|
+
// the log claimed it started empty — the disk path stages a fork for exactly this reason.
|
|
276
|
+
const parent = inherit ? live.get(inherit.parentSession) : undefined;
|
|
277
|
+
if (inherit && !parent) {
|
|
278
|
+
log.warn(`[fastagent] session "${sessionId}" names parent "${inherit.parentSession}", which has no record — starting empty`);
|
|
279
|
+
}
|
|
280
|
+
let created;
|
|
281
|
+
if (inherit && parent) {
|
|
282
|
+
try {
|
|
283
|
+
const staged = fresh();
|
|
284
|
+
const cut = inheritanceCut(reconcileInterruptedToolCalls(parent), inherit.branchHints);
|
|
285
|
+
if (cut)
|
|
286
|
+
copyBranchInto(parent, staged, cut.at);
|
|
287
|
+
created = staged;
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
log.warn(`[fastagent] could not inherit from "${inherit.parentSession}" into "${sessionId}" (${String(error)}) — starting empty`);
|
|
291
|
+
created = fresh(); // the partially-copied one is discarded, never registered
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
created = fresh();
|
|
296
|
+
}
|
|
297
|
+
live.set(sessionId, created);
|
|
298
|
+
return created;
|
|
299
|
+
},
|
|
300
|
+
async openIfExists(sessionId) {
|
|
301
|
+
return live.get(sessionId);
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** The spelling used before this store existed — read-only, so older records still resolve. */
|
|
306
|
+
function legacySessionId(sessionId) {
|
|
307
|
+
return sessionId.replace(/[^A-Za-z0-9._-]/g, (c) => {
|
|
308
|
+
const code = c.charCodeAt(0);
|
|
309
|
+
return code < 0x100
|
|
310
|
+
? `%${code.toString(16).toUpperCase().padStart(2, "0")}`
|
|
311
|
+
: `%u${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* closures. Deploy-time ambients a tool closes over at build time do NOT belong here.
|
|
5
5
|
*/
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
-
import type {
|
|
7
|
+
import type { SessionEntry as PiSessionEntry } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
/** FastAgent's read-only port over the current conversation manager. Serving and chat adapt their
|
|
9
10
|
* different concrete session implementations to this one tool-runtime contract. */
|
|
10
11
|
export interface ReadonlySessionManager {
|
|
@@ -13,13 +14,22 @@ export interface ReadonlySessionManager {
|
|
|
13
14
|
id: string;
|
|
14
15
|
timestamp: string;
|
|
15
16
|
}>;
|
|
16
|
-
getBranch(): Promise<
|
|
17
|
+
getBranch(): Promise<PiSessionEntry[]>;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* pi's AgentSession as the port above — the SAME adapter for both of its consumers: chat's resident
|
|
21
|
+
* session (session-builder.ts) and serving's per-invoke one (agent-session-factory.ts).
|
|
22
|
+
*
|
|
23
|
+
* `sessionId` is the CALLER's, not pi's. A tool correlates its own state by the id the channel
|
|
24
|
+
* minted; pi's is that id encoded into a filename-safe record name, and leaking the encoding here
|
|
25
|
+
* would hand a telegram tool `s-1001234567890` for a room it knows as `-1001234567890`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function agentSessionManager(session: AgentSession, sessionId: string): ReadonlySessionManager;
|
|
28
|
+
/**
|
|
29
|
+
* The turn's tool-activation bridge — narrow closures over the CURRENT session (bound per turn), so
|
|
30
|
+
* a loader tool can activate deferred tools mid-turn without tool.ts importing the engine. pi records
|
|
31
|
+
* the change in the session (`active_tools_change`) and the per-invoke restore
|
|
32
|
+
* (agent-session-factory.ts) carries it into later turns; defineTool's wrapper stamps the newly-activated names on
|
|
23
33
|
* the tool result (`addedToolNames`) — the load point native deferred-loading providers preserve the
|
|
24
34
|
* prompt-cache prefix with.
|
|
25
35
|
*/
|
|
@@ -41,12 +51,12 @@ export interface TurnContext {
|
|
|
41
51
|
/** Current conversation manager. Absent outside a FastAgent-managed agent turn. */
|
|
42
52
|
sessionManager?: ReadonlySessionManager;
|
|
43
53
|
/** Tool activation for the current turn. Two producers, one consumer surface: invoke.ts bridges the
|
|
44
|
-
*
|
|
54
|
+
* served session; chat.ts bridges the resident one (chat emulates deferral — same loader, same
|
|
45
55
|
* semantics). Absent only outside any turn (a bare `fastagent tool` run). */
|
|
46
56
|
tools?: ToolActivation;
|
|
47
57
|
}
|
|
48
58
|
export declare const turnContext: AsyncLocalStorage<TurnContext>;
|
|
49
|
-
/** The additive-activation contract, in ONE place for both bridges (
|
|
59
|
+
/** The additive-activation contract, in ONE place for both bridges (the served session,
|
|
50
60
|
* chat.ts over pi's AgentSession): dedupe → keep registered names only (pi's setters THROW on
|
|
51
61
|
* unknown) → exclude already-active → the names to actually add (empty = nothing to set). */
|
|
52
62
|
export declare function additiveActivation(registered: string[], current: string[], names: string[]): string[];
|
|
@@ -4,8 +4,30 @@
|
|
|
4
4
|
* closures. Deploy-time ambients a tool closes over at build time do NOT belong here.
|
|
5
5
|
*/
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
/**
|
|
8
|
+
* pi's AgentSession as the port above — the SAME adapter for both of its consumers: chat's resident
|
|
9
|
+
* session (session-builder.ts) and serving's per-invoke one (agent-session-factory.ts).
|
|
10
|
+
*
|
|
11
|
+
* `sessionId` is the CALLER's, not pi's. A tool correlates its own state by the id the channel
|
|
12
|
+
* minted; pi's is that id encoded into a filename-safe record name, and leaking the encoding here
|
|
13
|
+
* would hand a telegram tool `s-1001234567890` for a room it knows as `-1001234567890`.
|
|
14
|
+
*/
|
|
15
|
+
export function agentSessionManager(session, sessionId) {
|
|
16
|
+
return {
|
|
17
|
+
getSessionId: () => sessionId,
|
|
18
|
+
async getHeader() {
|
|
19
|
+
const header = session.sessionManager.getHeader();
|
|
20
|
+
if (!header)
|
|
21
|
+
throw new Error("session has no metadata header");
|
|
22
|
+
return { id: sessionId, timestamp: header.timestamp };
|
|
23
|
+
},
|
|
24
|
+
async getBranch() {
|
|
25
|
+
return session.sessionManager.getBranch();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
7
29
|
export const turnContext = new AsyncLocalStorage();
|
|
8
|
-
/** The additive-activation contract, in ONE place for both bridges (
|
|
30
|
+
/** The additive-activation contract, in ONE place for both bridges (the served session,
|
|
9
31
|
* chat.ts over pi's AgentSession): dedupe → keep registered names only (pi's setters THROW on
|
|
10
32
|
* unknown) → exclude already-active → the names to actually add (empty = nothing to set). */
|
|
11
33
|
export function additiveActivation(registered, current, names) {
|
|
@@ -11,7 +11,7 @@ export interface ToolContext {
|
|
|
11
11
|
sessionManager?: ReadonlySessionManager;
|
|
12
12
|
/** Tool activation for the current turn (a loader tool activates {@link DefineToolOptions.deferred}
|
|
13
13
|
* tools with it — the built-in `search_tools` is one consumer). Provided by both the serving path
|
|
14
|
-
* (
|
|
14
|
+
* (agent-session-factory.ts) and chat (session-builder.ts); undefined only outside any turn
|
|
15
15
|
* (a bare `fastagent tool` run). */
|
|
16
16
|
tools?: ToolActivation;
|
|
17
17
|
}
|
|
@@ -35,9 +35,9 @@ export interface DefineToolOptions<I extends z.ZodType> {
|
|
|
35
35
|
execute: (input: z.infer<I>, ctx: ToolContext) => unknown | Promise<unknown>;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* A tool as MOUNTED: what the
|
|
39
|
-
* purpose —
|
|
40
|
-
*
|
|
38
|
+
* A tool as MOUNTED: what the engine actually runs. Wider than the authored {@link AgentTool} on
|
|
39
|
+
* purpose — a lower-level mounted tool may read an `ExecutionEnv` from the fifth `execute` parameter,
|
|
40
|
+
* while coding-agent's cwd-bound tools ignore it and fastagent's authored tools take four arguments.
|
|
41
41
|
* Naming the wider type is what lets `defineTool` stay context-free for authors while both families
|
|
42
42
|
* live in one array; every helper that only inspects or reorders tools is typed on THIS.
|
|
43
43
|
*/
|
|
@@ -73,7 +73,7 @@ export declare function loadTools(dir: string): Promise<{
|
|
|
73
73
|
failures: ModuleLoadFailure[];
|
|
74
74
|
}>;
|
|
75
75
|
/**
|
|
76
|
-
* Merge resolved tools (pi
|
|
76
|
+
* Merge resolved tools (pi coding tools + `config.tools`) with discovered `tools/`, deduped by name.
|
|
77
77
|
* Existing tools win; dropped discovered tools surface as collisions.
|
|
78
78
|
*/
|
|
79
79
|
export declare function mergeDiscoveredTools(existing: MountedTool[], discovered: AgentTool[]): {
|
package/dist/engines/pi/tool.js
CHANGED
|
@@ -116,7 +116,7 @@ export async function loadTools(dir) {
|
|
|
116
116
|
return { tools: [...byName.values()], collisions, failures };
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
|
-
* Merge resolved tools (pi
|
|
119
|
+
* Merge resolved tools (pi coding tools + `config.tools`) with discovered `tools/`, deduped by name.
|
|
120
120
|
* Existing tools win; dropped discovered tools surface as collisions.
|
|
121
121
|
*/
|
|
122
122
|
export function mergeDiscoveredTools(existing, discovered) {
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The turn mechanism's
|
|
3
|
-
* repo's sense — that term is reserved for code with no engine import at
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The turn mechanism's ENGINE-agnostic half: the parts that describe a turn rather than pi. Not
|
|
3
|
+
* "engine-neutral" in this repo's sense — that term is reserved for code with no engine import at
|
|
4
|
+
* all (src/agent.ts), and the terminals here read pi's message shape. What they do not touch is how
|
|
5
|
+
* a turn is driven, which is why they survived the engine change unaltered.
|
|
6
6
|
*
|
|
7
7
|
* Lease — single-writer concurrency floor (injectable port + in-process default)
|
|
8
8
|
* Terminals — a settled pi message or a thrown error → the SPEC terminal, `retryable` included
|
|
9
9
|
* EventQueue — push→pull plumbing for engines that emit events beside their result
|
|
10
10
|
* Prompt prep — SPEC images → pi's ImageContent
|
|
11
|
+
* Projection — the rich SessionEvent stream → the narrow SPEC one
|
|
12
|
+
* Observation — the seam a control-plane hub attaches to (RunControls + SessionObserver)
|
|
11
13
|
*
|
|
12
|
-
* What is NOT neutral —
|
|
13
|
-
*
|
|
14
|
+
* What is NOT neutral — pi's event vocabulary and how a turn is driven — stays in invoke-session.ts,
|
|
15
|
+
* the L0 that owns it.
|
|
14
16
|
*/
|
|
15
17
|
import type { AssistantMessage, ImageContent } from "@earendil-works/pi-ai";
|
|
16
18
|
import { type AgentEvent, type Prompt } from "../../agent.ts";
|
|
19
|
+
import type { SessionEvent } from "../../session.ts";
|
|
17
20
|
export type Release = () => void;
|
|
18
21
|
export interface Lease {
|
|
19
22
|
/** Try to acquire exclusive write access for the session (fail-fast). Returns null if held. */
|
|
@@ -43,6 +46,29 @@ export declare function errorToTerminal(error: unknown): Extract<AgentEvent, {
|
|
|
43
46
|
export declare function toPiPromptOptions(prompt: Prompt): Promise<{
|
|
44
47
|
images?: ImageContent[];
|
|
45
48
|
} | undefined>;
|
|
49
|
+
/** Live modulation handles for one active run — what the control plane's `dispatch` routes to.
|
|
50
|
+
* Built inside the turn (it owns the engine instance); registered with the observer at
|
|
51
|
+
* run_started, gone after run_settled. RACE WINDOW (all three commands, symmetric): the run may
|
|
52
|
+
* resolve between the settled-check and the engine call landing — an accepted `abort` can still
|
|
53
|
+
* settle `completed`, and an accepted `steer`/`followUp` can settle without the prompt ever being
|
|
54
|
+
* consumed. Acceptance is not outcome; the settlement is the truth. */
|
|
55
|
+
export interface RunControls {
|
|
56
|
+
steer(prompt: Prompt): Promise<void>;
|
|
57
|
+
followUp(prompt: Prompt): Promise<void>;
|
|
58
|
+
abort(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
/** The DATA-plane observation seam: every rich event of every run, pushed as it happens. `run`
|
|
61
|
+
* carries the live {@link RunControls}, attached to the `run_started` event only. A hub
|
|
62
|
+
* (session-control.ts) implements this to serve `events()`/`state()`/`dispatch`; absent = zero
|
|
63
|
+
* overhead. Scope: RUN events only — the hub's own boundary-mutation events (`state_changed`,
|
|
64
|
+
* `compaction_*`) originate in the hub and reach full-vocabulary taps via the hub's `tap` option,
|
|
65
|
+
* not this seam. TRUST BOUNDARY: this seam hands every wired observer the run's modulation handles — it is the trusted hub seam, not a public fan-out point. Do not wire
|
|
66
|
+
* untrusted taps here; give third parties the read-only `events()` stream instead. */
|
|
67
|
+
export type SessionObserver = (session: string, event: SessionEvent, run?: RunControls) => void;
|
|
68
|
+
/** The SPEC projection of the rich stream. Events with no `AgentEvent` counterpart (progress,
|
|
69
|
+
* message boundaries, run boundaries) project to null — the invoke terminal is produced from the
|
|
70
|
+
* resolved message ({@link toTerminal}), not from `run_settled`. */
|
|
71
|
+
export declare function projectAgentEvent(se: SessionEvent): AgentEvent | null;
|
|
46
72
|
export declare class EventQueue<T> {
|
|
47
73
|
private buffer;
|
|
48
74
|
private wake?;
|
|
@@ -82,7 +82,7 @@ function messageSignal(message) {
|
|
|
82
82
|
*/
|
|
83
83
|
export function toTerminal(message) {
|
|
84
84
|
if (message.stopReason === "aborted") {
|
|
85
|
-
// A deliberate stop (control-plane
|
|
85
|
+
// A deliberate stop (a control-plane or consumer abort), not an error — see {@link ABORTED_CODE}
|
|
86
86
|
// for the consumer contract (design §6).
|
|
87
87
|
const details = message.errorMessage ?? "run aborted";
|
|
88
88
|
return { type: "failed", details, retryable: false, code: ABORTED_CODE };
|
|
@@ -119,6 +119,33 @@ export async function toPiPromptOptions(prompt) {
|
|
|
119
119
|
}));
|
|
120
120
|
return { images };
|
|
121
121
|
}
|
|
122
|
+
/** The SPEC projection of the rich stream. Events with no `AgentEvent` counterpart (progress,
|
|
123
|
+
* message boundaries, run boundaries) project to null — the invoke terminal is produced from the
|
|
124
|
+
* resolved message ({@link toTerminal}), not from `run_settled`. */
|
|
125
|
+
export function projectAgentEvent(se) {
|
|
126
|
+
switch (se.type) {
|
|
127
|
+
case "message_delta": {
|
|
128
|
+
const d = se.data;
|
|
129
|
+
return d.channel === "text" ? { type: "text", delta: d.delta } : { type: "thinking", delta: d.delta };
|
|
130
|
+
}
|
|
131
|
+
case "tool_started": {
|
|
132
|
+
const d = se.data;
|
|
133
|
+
return { type: "tool_started", id: d.id, name: d.name, args: d.args };
|
|
134
|
+
}
|
|
135
|
+
case "tool_finished": {
|
|
136
|
+
const d = se.data;
|
|
137
|
+
return { type: "tool_ended", id: d.id, isError: d.isError, content: d.content };
|
|
138
|
+
}
|
|
139
|
+
case "retry_scheduled": {
|
|
140
|
+
// `operation` (compaction | branch_summary) stays session-plane vocabulary — a turn renderer
|
|
141
|
+
// only needs "transient failure, retrying"; the engine detail lives in the control plane.
|
|
142
|
+
const d = se.data;
|
|
143
|
+
return { type: "retrying", attempt: d.attempt, maxAttempts: d.maxAttempts, delayMs: d.delayMs, reason: d.error };
|
|
144
|
+
}
|
|
145
|
+
default:
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
122
149
|
// ── EventQueue: push→pull plumbing for a two-port engine ────────────────────
|
|
123
150
|
//
|
|
124
151
|
// Single-consumer async queue; single-threaded JS means no await interleaves between push and
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The built-in `wake` tool: the agent's self-scheduling surface. Calling it records a one-shot wake-up
|
|
3
3
|
* (wakeups.ts); the scheduler fires it back into the SAME session, so the agent resumes THIS
|
|
4
4
|
* conversation after a delay ("check the deploy in 10 minutes"). The session comes from the turn
|
|
5
|
-
* context (ToolContext.sessionManager, set around the
|
|
5
|
+
* context (ToolContext.sessionManager, set around the turn); the state root is closed over at build
|
|
6
6
|
* time (where it is known — the workspace opener), never read from the turn.
|
|
7
7
|
*
|
|
8
8
|
* Mounted by the opener ONLY when `config.selfSchedule` is on AND on the serving path (`dev`/`start`, where
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
// Contract/channel-only consumers should prefer `@fastagent-sh/fastagent/core`; pi-specific consumers
|
|
3
3
|
// may use `@fastagent-sh/fastagent/pi`. The root remains the supported all-in-one surface.
|
|
4
4
|
export * from "./core.js";
|
|
5
|
+
export * from "./node.js";
|
|
5
6
|
export * from "./session.js";
|
|
6
7
|
export * from "./pi.js";
|
package/dist/log.d.ts
CHANGED
|
@@ -23,4 +23,7 @@ export declare function createLogger(opts: {
|
|
|
23
23
|
/** Set the posture default. A valid `FASTAGENT_LOG_LEVEL` override, if present, wins and is not changed. */
|
|
24
24
|
export declare function setLogLevel(level: LogLevel): void;
|
|
25
25
|
/** The process logger. Runtime code imports this and calls `log.info(...)` etc. */
|
|
26
|
+
import type { ModuleLoadFailure } from "./loader.ts";
|
|
26
27
|
export declare const log: Logger;
|
|
28
|
+
/** A module the loader skipped, said once, the same way for tools, channels and schedules. */
|
|
29
|
+
export declare function reportModuleLoadFailures(failures: readonly ModuleLoadFailure[]): void;
|
package/dist/log.js
CHANGED
|
@@ -47,5 +47,9 @@ const emit = (level) => (msg) => {
|
|
|
47
47
|
if (ORDER[level] >= ORDER[currentLevel])
|
|
48
48
|
console.error(format(level, msg));
|
|
49
49
|
};
|
|
50
|
-
/** The process logger. Runtime code imports this and calls `log.info(...)` etc. */
|
|
51
50
|
export const log = { debug: emit("debug"), info: emit("info"), warn: emit("warn"), error: emit("error") };
|
|
51
|
+
/** A module the loader skipped, said once, the same way for tools, channels and schedules. */
|
|
52
|
+
export function reportModuleLoadFailures(failures) {
|
|
53
|
+
for (const f of failures)
|
|
54
|
+
log.warn(`[fastagent] ${f.label} failed to load, skipping it — ${f.message}`);
|
|
55
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binding a Fetch handler to a Node HTTP server.
|
|
3
|
+
*
|
|
4
|
+
* Its own entry point because it is the one piece of the neutral surface that is RUNTIME-specific:
|
|
5
|
+
* `@hono/node-server` bridges `node:http` ↔ Fetch, and that package is the only third-party weight
|
|
6
|
+
* anywhere behind `/core`. Keeping it here lets a channel package or another engine import the
|
|
7
|
+
* contract without pulling a Node HTTP bridge it will never call — and lets a non-Node runtime
|
|
8
|
+
* (Workers, Deno, Bun's own server) consume `/core` unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Engine-neutral is not the same as runtime-neutral, and the two properties give the surface its
|
|
11
|
+
* three layers: `/core` is both (contract, fetch-shaped kit), `/node` is engine-neutral only (this
|
|
12
|
+
* file: the assembly and the HTTP binding, which need a filesystem, a clock and an environment),
|
|
13
|
+
* `/pi` is neither (it names an engine).
|
|
14
|
+
*/
|
|
15
|
+
export { nodeListener, serveNode } from "./channels/serve.ts";
|
|
16
|
+
export { mountAgentService, type AgentService, type MountableAgent, type MountAgentServiceOptions, } from "./service.ts";
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binding a Fetch handler to a Node HTTP server.
|
|
3
|
+
*
|
|
4
|
+
* Its own entry point because it is the one piece of the neutral surface that is RUNTIME-specific:
|
|
5
|
+
* `@hono/node-server` bridges `node:http` ↔ Fetch, and that package is the only third-party weight
|
|
6
|
+
* anywhere behind `/core`. Keeping it here lets a channel package or another engine import the
|
|
7
|
+
* contract without pulling a Node HTTP bridge it will never call — and lets a non-Node runtime
|
|
8
|
+
* (Workers, Deno, Bun's own server) consume `/core` unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Engine-neutral is not the same as runtime-neutral, and the two properties give the surface its
|
|
11
|
+
* three layers: `/core` is both (contract, fetch-shaped kit), `/node` is engine-neutral only (this
|
|
12
|
+
* file: the assembly and the HTTP binding, which need a filesystem, a clock and an environment),
|
|
13
|
+
* `/pi` is neither (it names an engine).
|
|
14
|
+
*/
|
|
15
|
+
export { nodeListener, serveNode } from "./channels/serve.js";
|
|
16
|
+
// The assembly: a MountableAgent becomes a mounted service. Engine-neutral — it reads the SPEC
|
|
17
|
+
// contract plus three paths, so a second engine reuses it with its own opener — but NOT runtime
|
|
18
|
+
// neutral: it reads a directory, a cron and an environment. That is what makes this its entry
|
|
19
|
+
// rather than `/core`.
|
|
20
|
+
export { mountAgentService, } from "./service.js";
|
package/dist/paths.d.ts
CHANGED
|
@@ -142,3 +142,10 @@ export declare function resolveSecretsDir(dir: string, env?: NodeJS.ProcessEnv):
|
|
|
142
142
|
* A missing target is fine (nothing to guard yet).
|
|
143
143
|
*/
|
|
144
144
|
export declare function assertInsideAgentDir(agentDir: string, name: string): Promise<void>;
|
|
145
|
+
/**
|
|
146
|
+
* Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
|
|
147
|
+
* this OUT of the agent?" — the startup report's redeploy notes, `add`'s printed `.env` label, and the
|
|
148
|
+
* dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
|
|
149
|
+
* does not act on where a user's paths point.
|
|
150
|
+
*/
|
|
151
|
+
export declare function isUnderDir(targetPath: string, baseDir: string): boolean;
|