@fastagent-sh/fastagent 0.17.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -7
- package/dist/agent.d.ts +12 -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.d.ts +4 -2
- package/dist/channels/feishu/feishu-api.js +1 -1
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +45 -15
- package/dist/channels/feishu/invoke-turn.d.ts +9 -3
- package/dist/channels/feishu/invoke-turn.js +152 -34
- package/dist/channels/feishu/parse.js +6 -0
- 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 +16 -111
- 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} +18 -4
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +23 -8
- 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/kit/state.d.ts +6 -0
- package/dist/channels/{state.js → kit/state.js} +4 -24
- 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.d.ts → kit/thread-participants.d.ts} +7 -0
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +4 -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 +9 -6
- 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 -12
- 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 +2 -2
- 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/deploy.js +13 -5
- package/dist/cli/commands/dev.js +26 -14
- package/dist/cli/commands/fire.js +2 -2
- package/dist/cli/commands/info.js +28 -5
- package/dist/cli/commands/invoke.js +1 -1
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +64 -95
- 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 +5 -3
- package/dist/cli/shared.js +14 -7
- package/dist/collect.d.ts +30 -4
- package/dist/collect.js +39 -6
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/preflight.d.ts +8 -2
- package/dist/deploy/preflight.js +22 -4
- package/dist/deploy/secrets.d.ts +3 -0
- package/dist/deploy/secrets.js +6 -0
- package/dist/dev-supervisor.js +16 -4
- 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 -42
- package/dist/engines/pi/create.js +150 -75
- 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 +35 -0
- package/dist/engines/pi/invoke-session.js +357 -0
- package/dist/engines/pi/login.js +7 -4
- package/dist/engines/pi/models.d.ts +59 -11
- package/dist/engines/pi/models.js +64 -9
- 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 +88 -90
- package/dist/engines/pi/session-control.d.ts +11 -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/{invoke.d.ts → turn-kit.d.ts} +46 -45
- package/dist/engines/pi/turn-kit.js +188 -0
- 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 +13 -0
- package/dist/paths.js +16 -0
- package/dist/pi.d.ts +15 -8
- package/dist/pi.js +7 -6
- package/dist/scaffold/templates/fastagent.config.mjs +2 -0
- 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-remote.js +10 -2
- package/dist/session.d.ts +4 -2
- package/dist/session.js +1 -1
- package/package.json +14 -5
- package/dist/channels/state.d.ts +0 -16
- package/dist/engines/pi/harness.d.ts +0 -112
- package/dist/engines/pi/harness.js +0 -153
- package/dist/engines/pi/invoke.js +0 -618
- 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 -44
- package/dist/engines/pi/sessions.js +0 -182
- 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/{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/{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
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The image pipeline pi's `read` tool needs: normalize an unsupported format to PNG, resize below the
|
|
3
|
-
* inline limit, and hand back the hints that tell the model what it is looking at.
|
|
4
|
-
*
|
|
5
|
-
* pi-agent-core's `createReadTool` takes this as an INJECTED processor and does nothing without one —
|
|
6
|
-
* unlike pi-coding-agent's, which wires its private `processImage` internally. That function is not
|
|
7
|
-
* exported (nor reachable: the package's `exports` map has no deep paths), so this rebuilds it from the
|
|
8
|
-
* two halves that ARE public, `convertToPng` and `resizeImage`/`formatDimensionNote`.
|
|
9
|
-
*
|
|
10
|
-
* It is upstream logic restated, which is a real cost — without it `read` on a screenshot sends the raw
|
|
11
|
-
* bytes (measured: 7.48 MB of base64 where pi-coding-agent sends 3.48 MB, and no dimension note for the
|
|
12
|
-
* model's coordinate math), and a bmp is dropped entirely while the tool's own description still
|
|
13
|
-
* advertises it. test/tools-parity.test.ts compares this against pi-coding-agent's real `read` on both
|
|
14
|
-
* paths, so upstream changing the pipeline surfaces as a failing test rather than as drift.
|
|
15
|
-
*/
|
|
16
|
-
import { convertToPng, formatDimensionNote, resizeImage } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
/** Formats a provider takes inline as-is; everything else has to become a PNG first. */
|
|
18
|
-
const INLINE_MIME = {
|
|
19
|
-
"image/png": "image/png",
|
|
20
|
-
"image/jpeg": "image/jpeg",
|
|
21
|
-
"image/jpg": "image/jpeg",
|
|
22
|
-
"image/gif": "image/gif",
|
|
23
|
-
"image/webp": "image/webp",
|
|
24
|
-
};
|
|
25
|
-
/** The `read` tool's image processor. Matches pi-coding-agent's messages verbatim: they reach the model
|
|
26
|
-
* as tool output, so a reworded one is a different prompt, not a different implementation detail. */
|
|
27
|
-
export const readImageProcessor = async (bytes, mimeType, options) => {
|
|
28
|
-
const base = mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
|
|
29
|
-
const inline = INLINE_MIME[base];
|
|
30
|
-
let normalized;
|
|
31
|
-
if (inline) {
|
|
32
|
-
normalized = { bytes, mimeType: inline };
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
const png = await convertToPng(Buffer.from(bytes).toString("base64"), base);
|
|
36
|
-
if (!png)
|
|
37
|
-
return { ok: false, message: "[Image omitted: could not be converted to a supported inline image format.]" };
|
|
38
|
-
normalized = { bytes: Buffer.from(png.data, "base64"), mimeType: png.mimeType, convertedFrom: base };
|
|
39
|
-
}
|
|
40
|
-
const hints = [];
|
|
41
|
-
const converted = (to) => normalized.convertedFrom && normalized.convertedFrom !== to
|
|
42
|
-
? `[Image converted from ${normalized.convertedFrom} to ${to}.]`
|
|
43
|
-
: undefined;
|
|
44
|
-
if (!options.autoResizeImages) {
|
|
45
|
-
const hint = converted(normalized.mimeType);
|
|
46
|
-
if (hint)
|
|
47
|
-
hints.push(hint);
|
|
48
|
-
return { ok: true, data: Buffer.from(normalized.bytes).toString("base64"), mimeType: normalized.mimeType, hints };
|
|
49
|
-
}
|
|
50
|
-
const resized = await resizeImage(normalized.bytes, normalized.mimeType);
|
|
51
|
-
if (!resized)
|
|
52
|
-
return { ok: false, message: "[Image omitted: could not be resized below the inline image size limit.]" };
|
|
53
|
-
const hint = converted(resized.mimeType);
|
|
54
|
-
if (hint)
|
|
55
|
-
hints.push(hint);
|
|
56
|
-
// The scale factor the model needs to map coordinates back onto the original — dropping it is what
|
|
57
|
-
// makes a resized screenshot unusable for anything positional.
|
|
58
|
-
const note = formatDimensionNote(resized);
|
|
59
|
-
if (note)
|
|
60
|
-
hints.push(note);
|
|
61
|
-
return { ok: true, data: resized.data, mimeType: resized.mimeType, hints };
|
|
62
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { Session, SessionTreeEntry } from "@earendil-works/pi-agent-core";
|
|
2
|
-
/** What fastagent needs from a session backend: open-or-create by opaque id. */
|
|
3
|
-
export interface PiSessionStore {
|
|
4
|
-
openOrCreate(sessionId: string): Promise<Session>;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* OPEN-EXISTING sibling of {@link PiSessionStore} (session-control.ts): an unknown session answers
|
|
8
|
-
* `undefined`, never creates one — sessions are the data plane's monopoly. Two consumers:
|
|
9
|
-
* - the OBSERVATION plane (`state()`/`entries()`), strictly read-only (design §16 invariant 4);
|
|
10
|
-
* - the control plane's BOUNDARY writers (`set_model`/`set_thinking` append override records to the
|
|
11
|
-
* returned handle; `navigate` moves its leaf) after an existence check, under the run lease.
|
|
12
|
-
* `openIfExists` skips the open-time crash reconciliation (that appends repair entries — a write
|
|
13
|
-
* the observation plane must not perform). The boundary writers are safe WITHOUT it for two
|
|
14
|
-
* different reasons: an override record is not a message, so it cannot create or pair with a
|
|
15
|
-
* dangling tool_use; a `navigate` writes no message either, but it CAN expose one — parking the
|
|
16
|
-
* leaf on an assistant entry whose tool results are now off-path is the dangling-pair state
|
|
17
|
-
* {@link reconcileInterruptedToolCalls} exists for. That is repaired at the next `openOrCreate`,
|
|
18
|
-
* which repairs AT THE LEAF — exactly where a move puts it.
|
|
19
|
-
*
|
|
20
|
-
* Writing MESSAGE-class records through this handle would bypass that repair: use `openOrCreate`
|
|
21
|
-
* for anything that enters the transcript.
|
|
22
|
-
*/
|
|
23
|
-
export interface PiSessionReader {
|
|
24
|
-
openIfExists(sessionId: string): Promise<Session | undefined>;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* The entries on the session's ACTIVE path, root→leaf — what every last-wins read must walk.
|
|
28
|
-
* `getEntries()` is the whole TREE: once `navigate` can move the leaf, the journal still carries
|
|
29
|
-
* the abandoned branch, and reading it flat would run the session on a setting it moved away from.
|
|
30
|
-
* Deliberately NOT `Session.getBranch()`: that walk is bounded by the last compaction's retained
|
|
31
|
-
* window, which is the right bound for MODEL CONTEXT and the wrong one for settings — an override
|
|
32
|
-
* recorded before a compaction is a preference, and it still governs the session after one.
|
|
33
|
-
*/
|
|
34
|
-
export declare function activePathEntries(session: Session): Promise<SessionTreeEntry[]>;
|
|
35
|
-
/** In-process store (pi InMemorySessionRepo). Continuity lives and dies with the instance. */
|
|
36
|
-
export declare function inMemorySessionStore(): PiSessionStore & PiSessionReader;
|
|
37
|
-
/**
|
|
38
|
-
* Disk-backed store (pi JsonlSessionRepo under `dir`): restart the process, conversations continue.
|
|
39
|
-
* `cwd` is recorded in session metadata; defaults to process.cwd().
|
|
40
|
-
*/
|
|
41
|
-
export declare function jsonlSessionStore(options: {
|
|
42
|
-
dir: string;
|
|
43
|
-
cwd?: string;
|
|
44
|
-
}): PiSessionStore & PiSessionReader;
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session persistence — the K-axis port and its first two backends.
|
|
3
|
-
*
|
|
4
|
-
* PiSessionStore is the consumer-owned port: open-or-create by opaque session id, nothing more.
|
|
5
|
-
* pi's full SessionRepo surface (list/open/create/delete/fork) stays behind the adapters. The `Pi`
|
|
6
|
-
* prefix is honest — `openOrCreate` returns pi's `Session`, so this is pi-coupled, not a neutral
|
|
7
|
-
* persistence contract.
|
|
8
|
-
*
|
|
9
|
-
* Continuity = same backing store + same session id: in-memory continuity dies with the instance;
|
|
10
|
-
* jsonl survives process restarts (disk is the truth).
|
|
11
|
-
*/
|
|
12
|
-
import { InMemorySessionRepo } from "@earendil-works/pi-agent-core";
|
|
13
|
-
import { JsonlSessionRepo, NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
14
|
-
import { log } from "../../log.js";
|
|
15
|
-
/**
|
|
16
|
-
* Crash-safety reconciliation, run on every OPEN of an existing session.
|
|
17
|
-
*
|
|
18
|
-
* A turn that dies mid tool-execution leaves an assistant `tool_use` with no matching result (pi
|
|
19
|
-
* persists the assistant message before the tool runs). The next turn would then hand the provider an
|
|
20
|
-
* `assistant(tool_use) -> user` sequence that Anthropic/OpenAI reject — the session is poisoned. We
|
|
21
|
-
* append an honest "interrupted" error result for each dangling call, restoring a valid transcript.
|
|
22
|
-
* Tool side-effect idempotency stays the tool's responsibility (SPEC §6); this only restores
|
|
23
|
-
* transcript validity, not exactly-once execution.
|
|
24
|
-
*
|
|
25
|
-
* Pairing is TURN-LOCAL: a tool_use is paired only by a toolResult that immediately follows it (up to
|
|
26
|
-
* the next non-toolResult). tool-call ids are not unique across turns (a local model may restart ids
|
|
27
|
-
* each response), so matching against the whole transcript could falsely settle a leaf call against an
|
|
28
|
-
* earlier turn's identical id. Append-only logs can only repair a gap AT THE LEAF (last assistant
|
|
29
|
-
* followed by nothing but its own results); an earlier gap is surfaced via log.warn rather than
|
|
30
|
-
* "fixed" with an orphaned result that appending cannot place.
|
|
31
|
-
*
|
|
32
|
-
* The synthetic result splits its audiences: `content` (read by the model, may reach the end user)
|
|
33
|
-
* stays neutral — it must NOT say "aborted" (pi's word for a user cancellation) or leak infra detail;
|
|
34
|
-
* `details` carries the operational marker for developers and is never sent to the provider.
|
|
35
|
-
*/
|
|
36
|
-
async function reconcileInterruptedToolCalls(session) {
|
|
37
|
-
const { messages } = await session.buildContext();
|
|
38
|
-
let leafIdx = -1;
|
|
39
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
40
|
-
if (messages[i]?.role === "assistant") {
|
|
41
|
-
leafIdx = i;
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (leafIdx === -1)
|
|
46
|
-
return; // no assistant turn yet
|
|
47
|
-
const leafReparable = messages.slice(leafIdx + 1).every((m) => m.role === "toolResult");
|
|
48
|
-
const toRepair = [];
|
|
49
|
-
const orphaned = [];
|
|
50
|
-
messages.forEach((m, idx) => {
|
|
51
|
-
if (m.role !== "assistant")
|
|
52
|
-
return;
|
|
53
|
-
const paired = new Set();
|
|
54
|
-
for (let j = idx + 1; j < messages.length; j++) {
|
|
55
|
-
const next = messages[j];
|
|
56
|
-
if (next?.role !== "toolResult")
|
|
57
|
-
break;
|
|
58
|
-
paired.add(next.toolCallId);
|
|
59
|
-
}
|
|
60
|
-
for (const block of m.content) {
|
|
61
|
-
if (block.type !== "toolCall" || paired.has(block.id))
|
|
62
|
-
continue;
|
|
63
|
-
if (idx === leafIdx && leafReparable)
|
|
64
|
-
toRepair.push({ id: block.id, name: block.name });
|
|
65
|
-
else
|
|
66
|
-
orphaned.push(block.id);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
if (orphaned.length > 0) {
|
|
70
|
-
log.warn(`[fastagent] unmatched tool_use is not at the session leaf; leaving it unreconciled ` +
|
|
71
|
-
`(an append-only log cannot repair a mid-history gap): toolCallIds=${orphaned.join(",")}`);
|
|
72
|
-
}
|
|
73
|
-
for (const { id, name } of toRepair) {
|
|
74
|
-
const result = {
|
|
75
|
-
role: "toolResult",
|
|
76
|
-
toolCallId: id,
|
|
77
|
-
toolName: name,
|
|
78
|
-
content: [
|
|
79
|
-
{
|
|
80
|
-
type: "text",
|
|
81
|
-
text: "This tool call did not complete and its result is unavailable. Re-run it if the result is still needed.",
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
details: { fastagent: "interrupted-tool-call" },
|
|
85
|
-
isError: true,
|
|
86
|
-
timestamp: Date.now(),
|
|
87
|
-
};
|
|
88
|
-
await session.appendMessage(result);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* The entries on the session's ACTIVE path, root→leaf — what every last-wins read must walk.
|
|
93
|
-
* `getEntries()` is the whole TREE: once `navigate` can move the leaf, the journal still carries
|
|
94
|
-
* the abandoned branch, and reading it flat would run the session on a setting it moved away from.
|
|
95
|
-
* Deliberately NOT `Session.getBranch()`: that walk is bounded by the last compaction's retained
|
|
96
|
-
* window, which is the right bound for MODEL CONTEXT and the wrong one for settings — an override
|
|
97
|
-
* recorded before a compaction is a preference, and it still governs the session after one.
|
|
98
|
-
*/
|
|
99
|
-
export async function activePathEntries(session) {
|
|
100
|
-
// LEAF FIRST, then the journal — the order is load-bearing, not incidental: `getEntries()` is a
|
|
101
|
-
// SNAPSHOT, so reading it first would let any concurrent append (i.e. any turn in progress) leave
|
|
102
|
-
// a leaf the snapshot cannot contain, and the integrity throw below would fire on a live session
|
|
103
|
-
// instead of a corrupt one. This way the snapshot is always a superset of the leaf's chain.
|
|
104
|
-
const leafId = await session.getLeafId();
|
|
105
|
-
const entries = await session.getEntries();
|
|
106
|
-
// A null leaf is pi's ROOT position (what `moveTo(null)` sets), so the active path is EMPTY — not
|
|
107
|
-
// "the whole journal", which on a branched session would mean every abandoned branch at once.
|
|
108
|
-
if (leafId === null)
|
|
109
|
-
return [];
|
|
110
|
-
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
111
|
-
// A chain that is not intact THROWS: any other disposition returns a short path that reads like a
|
|
112
|
-
// short session (every override and activation above the gap gone, the next turn silently on
|
|
113
|
-
// assembly defaults). pi's storages already reject a leaf outside the journal in `getLeafId()`,
|
|
114
|
-
// but the port is swappable — so the walk names it here rather than dying on `undefined.parentId`.
|
|
115
|
-
const start = byId.get(leafId);
|
|
116
|
-
if (!start)
|
|
117
|
-
throw new Error(`session leaf "${leafId}" is missing from the journal`);
|
|
118
|
-
const path = [];
|
|
119
|
-
for (let cur = start;;) {
|
|
120
|
-
path.push(cur);
|
|
121
|
-
// A path cannot be longer than the journal it walks; anything more is a parentId cycle, which
|
|
122
|
-
// would otherwise hang the turn with no `failed` event at all.
|
|
123
|
-
if (path.length > entries.length)
|
|
124
|
-
throw new Error(`session entry "${cur.id}" cycles through its parents`);
|
|
125
|
-
if (!cur.parentId)
|
|
126
|
-
break;
|
|
127
|
-
const parent = byId.get(cur.parentId);
|
|
128
|
-
if (!parent)
|
|
129
|
-
throw new Error(`session entry "${cur.parentId}" is missing from the journal (parent of "${cur.id}")`);
|
|
130
|
-
cur = parent;
|
|
131
|
-
}
|
|
132
|
-
return path.reverse(); // walked leaf→root; every consumer reads it root→leaf
|
|
133
|
-
}
|
|
134
|
-
/** In-process store (pi InMemorySessionRepo). Continuity lives and dies with the instance. */
|
|
135
|
-
export function inMemorySessionStore() {
|
|
136
|
-
const repo = new InMemorySessionRepo();
|
|
137
|
-
return {
|
|
138
|
-
async openOrCreate(sessionId) {
|
|
139
|
-
const existing = (await repo.list()).find((m) => m.id === sessionId);
|
|
140
|
-
if (!existing)
|
|
141
|
-
return repo.create({ id: sessionId });
|
|
142
|
-
const session = await repo.open(existing);
|
|
143
|
-
await reconcileInterruptedToolCalls(session);
|
|
144
|
-
return session;
|
|
145
|
-
},
|
|
146
|
-
async openIfExists(sessionId) {
|
|
147
|
-
const existing = (await repo.list()).find((m) => m.id === sessionId);
|
|
148
|
-
return existing ? repo.open(existing) : undefined;
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Disk-backed store (pi JsonlSessionRepo under `dir`): restart the process, conversations continue.
|
|
154
|
-
* `cwd` is recorded in session metadata; defaults to process.cwd().
|
|
155
|
-
*/
|
|
156
|
-
export function jsonlSessionStore(options) {
|
|
157
|
-
const cwd = options.cwd ?? process.cwd();
|
|
158
|
-
const repo = new JsonlSessionRepo({ fs: new NodeExecutionEnv({ cwd }), sessionsRoot: options.dir });
|
|
159
|
-
return {
|
|
160
|
-
async openOrCreate(sessionId) {
|
|
161
|
-
// Caller-provided ids land in jsonl FILENAMES — encode anything unsafe before it reaches disk.
|
|
162
|
-
const id = encodeSessionId(sessionId);
|
|
163
|
-
// Scope the lookup to this store's cwd: two stores sharing a sessionsRoot must not open each
|
|
164
|
-
// other's sessions (pi groups sessions by project dir).
|
|
165
|
-
const existing = (await repo.list({ cwd })).find((m) => m.id === id);
|
|
166
|
-
if (!existing)
|
|
167
|
-
return repo.create({ id, cwd });
|
|
168
|
-
const session = await repo.open(existing);
|
|
169
|
-
await reconcileInterruptedToolCalls(session);
|
|
170
|
-
return session;
|
|
171
|
-
},
|
|
172
|
-
async openIfExists(sessionId) {
|
|
173
|
-
const id = encodeSessionId(sessionId);
|
|
174
|
-
const existing = (await repo.list({ cwd })).find((m) => m.id === id);
|
|
175
|
-
return existing ? repo.open(existing) : undefined;
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
/** Injective filename-safe encoding: [A-Za-z0-9._-] verbatim, the rest %-escaped. */
|
|
180
|
-
function encodeSessionId(id) {
|
|
181
|
-
return id.replace(/[^A-Za-z0-9._-]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
182
|
-
}
|
package/dist/host/node.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Node host (K-axis): mount a route table of Fetch handlers on a node:http server. Post-ACK work
|
|
3
|
-
* (e.g. a webhook channel's fire-and-forget turns) runs on this process's event loop and is lost on
|
|
4
|
-
* shutdown (the accepted tradeoff until durable execution exists).
|
|
5
|
-
*/
|
|
6
|
-
import { createServer } from "node:http";
|
|
7
|
-
import { nodeListener } from "../channels/http.js";
|
|
8
|
-
import { text } from "../channels/respond.js";
|
|
9
|
-
/** Parse a route key: `"METHOD /path"` → `{ method, path }`, or `"/path"` → `{ path }` (any method). */
|
|
10
|
-
export function parseRouteKey(key) {
|
|
11
|
-
const sp = key.indexOf(" ");
|
|
12
|
-
return sp === -1 ? { path: key } : { method: key.slice(0, sp).toUpperCase(), path: key.slice(sp + 1) };
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Compose a {@link Routes} table into one handler: exact pathname match (optionally method-qualified),
|
|
16
|
-
* 405 when the path exists under another method, 404 otherwise. No params/wildcards.
|
|
17
|
-
*/
|
|
18
|
-
export function router(routes) {
|
|
19
|
-
const entries = Object.entries(routes).map(([key, handler]) => ({
|
|
20
|
-
...parseRouteKey(key),
|
|
21
|
-
handler,
|
|
22
|
-
}));
|
|
23
|
-
return (req) => {
|
|
24
|
-
const { pathname } = new URL(req.url);
|
|
25
|
-
const onPath = entries.filter((e) => e.path === pathname);
|
|
26
|
-
if (onPath.length === 0)
|
|
27
|
-
return text("not found\n", 404);
|
|
28
|
-
const match = onPath.find((e) => e.method === undefined || e.method === req.method);
|
|
29
|
-
if (!match)
|
|
30
|
-
return text("method not allowed\n", 405);
|
|
31
|
-
return match.handler(req);
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller stop
|
|
36
|
-
* accepting or force-close active connections — no logging/signals/exit (the CLI owns those).
|
|
37
|
-
* `listening` resolves with the bound port (useful for port 0) or rejects on a bind error.
|
|
38
|
-
* `host` is the bind address; unset means all interfaces (what containers need).
|
|
39
|
-
*/
|
|
40
|
-
export function serveNode(handler, options) {
|
|
41
|
-
const server = createServer(nodeListener(async (req) => handler(req)));
|
|
42
|
-
const listening = new Promise((resolve, reject) => {
|
|
43
|
-
server.once("error", reject); // a bind failure surfaces here, before "listening"
|
|
44
|
-
server.listen({ port: options.port, host: options.host }, () => {
|
|
45
|
-
server.off("error", reject);
|
|
46
|
-
resolve(server.address().port);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
const close = () => new Promise((resolve, reject) => server.close((e) => (e ? reject(e) : resolve())));
|
|
50
|
-
const closeAllConnections = () => server.closeAllConnections();
|
|
51
|
-
return { listening, close, closeAllConnections };
|
|
52
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|