@fastagent-sh/fastagent 0.13.0 → 0.15.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 +5 -5
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/context-buffer.d.ts +30 -0
- package/dist/channels/context-buffer.js +98 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +43 -0
- package/dist/channels/feishu/context-buffer.js +72 -0
- package/dist/channels/feishu/crypto.d.ts +4 -2
- package/dist/channels/feishu/crypto.js +4 -2
- package/dist/channels/feishu/feishu-api.d.ts +15 -7
- package/dist/channels/feishu/feishu-api.js +22 -4
- package/dist/channels/feishu/feishu.d.ts +38 -16
- package/dist/channels/feishu/feishu.js +286 -151
- package/dist/channels/feishu/invoke-turn.d.ts +24 -31
- package/dist/channels/feishu/invoke-turn.js +61 -62
- package/dist/channels/feishu/model.d.ts +98 -0
- package/dist/channels/feishu/model.js +9 -0
- package/dist/channels/feishu/normalize.d.ts +23 -0
- package/dist/channels/feishu/normalize.js +132 -0
- package/dist/channels/feishu/owned-threads.d.ts +7 -0
- package/dist/channels/feishu/owned-threads.js +47 -0
- package/dist/channels/feishu/parse.d.ts +21 -103
- package/dist/channels/feishu/parse.js +35 -145
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +26 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +10 -3
- package/dist/channels/feishu/setup-mode.d.ts +8 -0
- package/dist/channels/feishu/setup-mode.js +2 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +8 -6
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +35 -2
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/lark/scaffold/channel.ts +10 -3
- package/dist/channels/preview-kit.d.ts +109 -0
- package/dist/channels/preview-kit.js +183 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/owned-threads.d.ts +6 -0
- package/dist/channels/slack/owned-threads.js +43 -0
- package/dist/channels/slack/parse.d.ts +23 -0
- package/dist/channels/slack/parse.js +81 -0
- package/dist/channels/slack/preview.d.ts +24 -0
- package/dist/channels/slack/preview.js +359 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +77 -0
- package/dist/channels/slack/slack-api.js +415 -0
- package/dist/channels/slack/slack.d.ts +58 -0
- package/dist/channels/slack/slack.js +451 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.js +3 -3
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +13 -0
- package/dist/channels/tasks.js +10 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/telegram.js +23 -9
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
- package/dist/channels/{feishu/text.js → text.js} +1 -1
- package/dist/channels/turn-queue.js +1 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +27 -0
- package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
- package/dist/cli/add-slack.d.ts +10 -0
- package/dist/cli/add-slack.js +204 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +13 -0
- package/dist/cli/commands/add.js +274 -0
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +559 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.js +21 -0
- package/dist/cli/commands/deploy.d.ts +15 -0
- package/dist/cli/commands/deploy.js +394 -0
- package/dist/cli/commands/dev.d.ts +11 -0
- package/dist/cli/commands/dev.js +82 -0
- package/dist/cli/commands/fire.d.ts +7 -0
- package/dist/cli/commands/fire.js +45 -0
- package/dist/cli/commands/info.d.ts +7 -0
- package/dist/cli/commands/info.js +108 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.js +81 -0
- package/dist/cli/commands/invoke.d.ts +7 -0
- package/dist/cli/commands/invoke.js +28 -0
- package/dist/cli/commands/login.d.ts +6 -0
- package/dist/cli/commands/login.js +52 -0
- package/dist/cli/commands/models.d.ts +1 -0
- package/dist/cli/commands/models.js +15 -0
- package/dist/cli/commands/schedule.d.ts +12 -0
- package/dist/cli/commands/schedule.js +89 -0
- package/dist/cli/commands/start.d.ts +10 -0
- package/dist/cli/commands/start.js +97 -0
- package/dist/cli/commands/tool.d.ts +1 -0
- package/dist/cli/commands/tool.js +38 -0
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +32 -0
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +90 -0
- package/dist/cli/kernel.js +190 -0
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.d.ts +11 -0
- package/dist/cli/program.js +479 -0
- package/dist/cli/serve.d.ts +48 -0
- package/dist/cli/serve.js +248 -0
- package/dist/cli/shared.d.ts +44 -0
- package/dist/cli/shared.js +246 -0
- package/dist/cli.js +8 -1329
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/docker/plan.d.ts +45 -0
- package/dist/deploy/docker/plan.js +141 -0
- package/dist/deploy/docker/run.d.ts +40 -0
- package/dist/deploy/docker/run.js +126 -0
- package/dist/deploy/fly/plan.d.ts +3 -1
- package/dist/deploy/fly/plan.js +12 -7
- package/dist/deploy/fly/run.d.ts +5 -4
- package/dist/deploy/fly/run.js +16 -5
- package/dist/deploy/preflight.d.ts +10 -4
- package/dist/deploy/preflight.js +57 -12
- package/dist/deploy/railway/plan.d.ts +3 -1
- package/dist/deploy/railway/plan.js +12 -4
- package/dist/deploy/railway/run.d.ts +5 -4
- package/dist/deploy/railway/run.js +15 -5
- package/dist/deploy/runner.d.ts +6 -2
- package/dist/deploy/runner.js +1 -0
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +0 -2
- package/dist/dev-supervisor.js +7 -3
- package/dist/engines/pi/auth.js +160 -46
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +90 -60
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -188
- package/dist/engines/pi/config.d.ts +23 -9
- package/dist/engines/pi/config.js +35 -5
- package/dist/engines/pi/create.d.ts +36 -7
- package/dist/engines/pi/create.js +63 -22
- package/dist/engines/pi/harness.d.ts +65 -1
- package/dist/engines/pi/harness.js +166 -2
- package/dist/engines/pi/invoke.d.ts +56 -3
- package/dist/engines/pi/invoke.js +340 -20
- package/dist/engines/pi/login.d.ts +11 -0
- package/dist/engines/pi/login.js +17 -5
- package/dist/engines/pi/models.d.ts +56 -10
- package/dist/engines/pi/models.js +61 -23
- package/dist/engines/pi/search-tools.d.ts +10 -0
- package/dist/engines/pi/search-tools.js +138 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +46 -11
- package/dist/engines/pi/tool-context.js +11 -9
- package/dist/engines/pi/tool.d.ts +35 -6
- package/dist/engines/pi/tool.js +47 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/engines/pi/workspace.d.ts +56 -1
- package/dist/engines/pi/workspace.js +75 -16
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +23 -6
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/pi.d.ts +6 -4
- package/dist/pi.js +2 -1
- package/dist/scaffold/add-channel.d.ts +19 -6
- package/dist/scaffold/add-channel.js +127 -16
- package/dist/scaffold/templates/fastagent.config.mjs +5 -3
- package/dist/schedule/wakeups.d.ts +0 -3
- package/dist/schedule/wakeups.js +1 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +21 -10
- package/package.json +22 -7
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { SSE_HEARTBEAT_MS } from "./channels/http.js";
|
|
2
|
+
import { abortFirstIterator } from "./collect.js";
|
|
3
|
+
/** Dead-connection watchdog for SSE reads: the server heartbeats every SSE_HEARTBEAT_MS, so a
|
|
4
|
+
* PENDING READ seeing no bytes (of ANY kind — comments included) for this many missed beats
|
|
5
|
+
* means the connection is a black hole. The stream is aborted and surfaced as an error, so a
|
|
6
|
+
* consumer's failure budget ticks instead of hanging forever. Quiet-but-alive streams (a long
|
|
7
|
+
* tool call) keep heartbeating and never trip this. */
|
|
8
|
+
const SSE_IDLE_LIMIT_MS = 3 * SSE_HEARTBEAT_MS;
|
|
9
|
+
function idleWatchdog(abort) {
|
|
10
|
+
let armedAt;
|
|
11
|
+
let stale = false;
|
|
12
|
+
const timer = setInterval(() => {
|
|
13
|
+
if (armedAt !== undefined && Date.now() - armedAt > SSE_IDLE_LIMIT_MS) {
|
|
14
|
+
stale = true;
|
|
15
|
+
abort.abort();
|
|
16
|
+
}
|
|
17
|
+
}, SSE_HEARTBEAT_MS);
|
|
18
|
+
return {
|
|
19
|
+
arm: () => {
|
|
20
|
+
armedAt ??= Date.now();
|
|
21
|
+
},
|
|
22
|
+
disarm: () => {
|
|
23
|
+
armedAt = undefined;
|
|
24
|
+
},
|
|
25
|
+
stale: () => stale,
|
|
26
|
+
stop: () => clearInterval(timer),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** A control request the server answered with a non-2xx status. Carries the STRUCTURED status so a
|
|
30
|
+
* consumer distinguishing auth failure (401 — stale token, unrecoverable) from transient transport
|
|
31
|
+
* trouble branches on `status`, never on message prose. */
|
|
32
|
+
export class ControlRequestError extends Error {
|
|
33
|
+
status;
|
|
34
|
+
constructor(status, body) {
|
|
35
|
+
super(`control request failed: ${status} ${body}`);
|
|
36
|
+
this.status = status;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Connect and return a remote `SessionControl`. Async because `capabilities()` is synchronous in
|
|
41
|
+
* the contract: the static declaration is fetched ONCE here and served from memory — which also
|
|
42
|
+
* makes a wrong URL/token fail at connect time, not on first use.
|
|
43
|
+
*/
|
|
44
|
+
export async function connectSessionControl(options) {
|
|
45
|
+
const { url, token, fetchFn = fetch } = options;
|
|
46
|
+
const base = url.replace(/\/$/, "");
|
|
47
|
+
const headers = { authorization: `Bearer ${token}` };
|
|
48
|
+
// Non-streaming requests carry a TIMEOUT: attach's whole reliability model counts failed rounds
|
|
49
|
+
// against a budget ("unreachable for ~Ns"), which a black-hole endpoint (firewall drop, half-dead
|
|
50
|
+
// tunnel) would silently defeat — a hung state()/entries() ticks nothing. The SSE stream stays
|
|
51
|
+
// timeout-free (quiet is normal there; heartbeats cover proxy idling).
|
|
52
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
53
|
+
// The PAYLOAD-bearing calls get a longer budget than the black-hole detector's 10s — in both
|
|
54
|
+
// directions: dispatch may UPLOAD up to the 1 MiB body cap (base64 images in steer/follow_up),
|
|
55
|
+
// and entries may DOWNLOAD a long session's full record (a cursor-less first backfill) — a slow
|
|
56
|
+
// link legitimately needs longer, and cutting a healthy transfer would be indistinguishable from
|
|
57
|
+
// a dead endpoint. capabilities/state stay on the short detector: they are small by contract.
|
|
58
|
+
const PAYLOAD_TIMEOUT_MS = 60_000;
|
|
59
|
+
const get = async (path, timeoutMs = REQUEST_TIMEOUT_MS) => {
|
|
60
|
+
const res = await fetchFn(`${base}${path}`, { headers, signal: AbortSignal.timeout(timeoutMs) });
|
|
61
|
+
if (!res.ok)
|
|
62
|
+
throw new ControlRequestError(res.status, await res.text());
|
|
63
|
+
return (await res.json());
|
|
64
|
+
};
|
|
65
|
+
const capabilities = await get("/control/capabilities");
|
|
66
|
+
return {
|
|
67
|
+
capabilities: () => capabilities,
|
|
68
|
+
state: (session) => get(`/control/state?session=${encodeURIComponent(session)}`),
|
|
69
|
+
entries: (session, opts) => get(`/control/entries?session=${encodeURIComponent(session)}${opts?.since !== undefined ? `&since=${encodeURIComponent(opts.since)}` : ""}`, PAYLOAD_TIMEOUT_MS),
|
|
70
|
+
async dispatch(session, command) {
|
|
71
|
+
const res = await fetchFn(`${base}/control/dispatch`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
74
|
+
body: JSON.stringify({ session, command }),
|
|
75
|
+
signal: AbortSignal.timeout(PAYLOAD_TIMEOUT_MS),
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new ControlRequestError(res.status, await res.text());
|
|
79
|
+
return (await res.json());
|
|
80
|
+
},
|
|
81
|
+
events(session) {
|
|
82
|
+
// Each ITERATION opens its own connection (gen/abort created inside asyncIterator), matching
|
|
83
|
+
// the local hub's "every iteration is a fresh subscription" — a shared single-use generator
|
|
84
|
+
// would make the second for-await silently empty, breaking local/remote isomorphism.
|
|
85
|
+
// The abort controller lives OUTSIDE the generator: a consumer's `return()`/`break` while the
|
|
86
|
+
// generator is suspended on a quiet SSE read must abort the fetch FIRST — an async generator's
|
|
87
|
+
// own finally only runs after the pending await settles, which a silent stream never does.
|
|
88
|
+
const openStream = (abort) => (async function* iterate() {
|
|
89
|
+
// Armed BEFORE the fetch: the connect phase (headers never arriving from a black-holed
|
|
90
|
+
// endpoint) is otherwise a window no timeout covers — the same watchdog terminates it,
|
|
91
|
+
// with headers-arrival counting as the first sign of life.
|
|
92
|
+
const watchdog = idleWatchdog(abort);
|
|
93
|
+
watchdog.arm(); // the connect await is a pending read
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetchFn(`${base}/control/events?session=${encodeURIComponent(session)}`, {
|
|
96
|
+
headers,
|
|
97
|
+
signal: abort.signal,
|
|
98
|
+
});
|
|
99
|
+
watchdog.disarm(); // headers arrived
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
// The error body is a pending read too — a half-dead tunnel serving 4xx headers then
|
|
102
|
+
// black-holing the body must not hang the round outside every budget. Re-armed: the
|
|
103
|
+
// watchdog aborts the read and the round fails with the dead-connection diagnosis.
|
|
104
|
+
watchdog.arm();
|
|
105
|
+
throw new ControlRequestError(res.status, await res.text());
|
|
106
|
+
}
|
|
107
|
+
if (!res.body)
|
|
108
|
+
throw new Error("control events: response has no body");
|
|
109
|
+
let nextSeq = 0;
|
|
110
|
+
for await (const data of sseData(res.body, watchdog)) {
|
|
111
|
+
// Parse discipline, same as the other two wire planes (dispatch parses, invoke
|
|
112
|
+
// classifies drift): a non-JSON or non-envelope payload is PROTOCOL MISMATCH —
|
|
113
|
+
// thrown, so a consumer's failure budget applies — never misdiagnosed as an
|
|
114
|
+
// in-transit gap whose remedy (reconnect) can never fix it.
|
|
115
|
+
let wire;
|
|
116
|
+
try {
|
|
117
|
+
// The ONE envelope type (control.ts's WireEvent) — an inline shape would let the
|
|
118
|
+
// envelope drift server-side while this cast silently kept the old fields.
|
|
119
|
+
wire = JSON.parse(data);
|
|
120
|
+
}
|
|
121
|
+
catch (parseError) {
|
|
122
|
+
throw new Error(`control events: non-JSON data on the stream (${String(parseError)}) — protocol mismatch?`);
|
|
123
|
+
}
|
|
124
|
+
if (typeof wire.seq !== "number" || typeof wire.event !== "object" || wire.event === null) {
|
|
125
|
+
throw new Error("control events: malformed envelope — the endpoint does not speak this protocol version");
|
|
126
|
+
}
|
|
127
|
+
// Envelope checks — consumed HERE. (epoch is not compared: it cannot change within
|
|
128
|
+
// one connection — see the header note.) A gap THROWS like a protocol mismatch: the
|
|
129
|
+
// consumer's failure path (budget, its own io) owns the diagnostic — a library-level
|
|
130
|
+
// log would bypass consumer output discipline, and a silent clean end would be
|
|
131
|
+
// indistinguishable from the server closing normally.
|
|
132
|
+
if (wire.seq !== nextSeq) {
|
|
133
|
+
throw new Error(`control events: sequence gap (expected ${nextSeq}, got ${wire.seq}) — events were lost in transit; resync via entries()`);
|
|
134
|
+
}
|
|
135
|
+
nextSeq = wire.seq + 1;
|
|
136
|
+
yield wire.event;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (abort.signal.aborted) {
|
|
141
|
+
if (watchdog.stale()) {
|
|
142
|
+
throw new Error(`control events: no bytes for ${SSE_IDLE_LIMIT_MS / 1000}s (heartbeats absent) — dead connection; resync via entries()`);
|
|
143
|
+
}
|
|
144
|
+
return; // the consumer walked away — clean end, not an error
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
watchdog.stop();
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
return {
|
|
153
|
+
[Symbol.asyncIterator]() {
|
|
154
|
+
const abort = new AbortController();
|
|
155
|
+
// Abort-first cancellation (see abortFirstIterator): aborting the connection unblocks a
|
|
156
|
+
// generator suspended on a quiet stream read.
|
|
157
|
+
return abortFirstIterator(openStream(abort), () => abort.abort());
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The remote DATA plane: an `Agent` whose `invoke` drives `POST /control/invoke` on a serving
|
|
165
|
+
* process — paired with {@link connectSessionControl}, a client holds a full remote fastagent
|
|
166
|
+
* instance through the same two contracts local code uses. A REAL Agent, failure discipline
|
|
167
|
+
* included: SPEC MUST 2 forbids iteration throws, so every failure — transport (401/refused/
|
|
168
|
+
* dropped mid-stream), protocol, and the images precheck — becomes a terminal `failed` event
|
|
169
|
+
* (`retryable` from the HTTP status where one exists; network trouble is retryable). Breaking out
|
|
170
|
+
* of iteration disconnects the request, which cancels the run (SPEC cancellation semantics travel
|
|
171
|
+
* the wire). The invoke wire is text-only for now: a prompt with images fails visibly instead of
|
|
172
|
+
* silently dropping them (steer/follow_up on the control plane carry full Prompts).
|
|
173
|
+
*/
|
|
174
|
+
export function connectAgent(options) {
|
|
175
|
+
const { url, token, fetchFn = fetch } = options;
|
|
176
|
+
const base = url.replace(/\/$/, "");
|
|
177
|
+
const toFailed = (error) => {
|
|
178
|
+
if (error instanceof ControlRequestError) {
|
|
179
|
+
return { type: "failed", details: error.message, retryable: error.status === 429 || error.status >= 500 };
|
|
180
|
+
}
|
|
181
|
+
return { type: "failed", details: String(error), retryable: true }; // network-class: worth re-sending
|
|
182
|
+
};
|
|
183
|
+
// COMPILE-TIME drift guard (dispatch-wire parity): the invoke body carries exactly text (and
|
|
184
|
+
// rejects images visibly) — a new Prompt field must break THIS line and force a decision
|
|
185
|
+
// (carry it or reject it), never vanish on the wire while the client believes it was sent.
|
|
186
|
+
const _invokeDriftGuard = {};
|
|
187
|
+
void _invokeDriftGuard;
|
|
188
|
+
// Same guard for Scope: the body carries session only — a new Scope field must force a decision.
|
|
189
|
+
const _scopeDriftGuard = {};
|
|
190
|
+
void _scopeDriftGuard;
|
|
191
|
+
return {
|
|
192
|
+
invoke(scope, prompt) {
|
|
193
|
+
const abort = new AbortController();
|
|
194
|
+
const openStream = () => (async function* iterate() {
|
|
195
|
+
if (prompt.images && prompt.images.length > 0) {
|
|
196
|
+
yield {
|
|
197
|
+
type: "failed",
|
|
198
|
+
details: "remote invoke does not carry images yet — send text, or invoke in-process",
|
|
199
|
+
retryable: false,
|
|
200
|
+
};
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// Exactly-one-terminal discipline across the wire: a drop AFTER the server's terminal
|
|
204
|
+
// must not append a second one (catch included), and a stream that ends WITHOUT one
|
|
205
|
+
// (server died mid-run) must be closed with a failed — never a terminal-less end.
|
|
206
|
+
let terminalSeen = false;
|
|
207
|
+
// Armed BEFORE the fetch — the run's driver must not hang on a black-holed connect
|
|
208
|
+
// either (the connect await is a pending read; headers arriving disarm it).
|
|
209
|
+
const watchdog = idleWatchdog(abort);
|
|
210
|
+
watchdog.arm();
|
|
211
|
+
try {
|
|
212
|
+
const res = await fetchFn(`${base}/control/invoke`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
215
|
+
body: JSON.stringify({ session: scope.session, text: prompt.text }),
|
|
216
|
+
signal: abort.signal,
|
|
217
|
+
});
|
|
218
|
+
watchdog.disarm(); // headers arrived
|
|
219
|
+
if (!res.ok) {
|
|
220
|
+
watchdog.arm(); // the error body is a pending read too — see the events() twin
|
|
221
|
+
const failure = toFailed(new ControlRequestError(res.status, await res.text()));
|
|
222
|
+
watchdog.disarm();
|
|
223
|
+
yield failure;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!res.body) {
|
|
227
|
+
yield { type: "failed", details: "remote invoke: response has no body", retryable: true };
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
for await (const data of sseData(res.body, watchdog)) {
|
|
231
|
+
let event;
|
|
232
|
+
try {
|
|
233
|
+
event = JSON.parse(data);
|
|
234
|
+
}
|
|
235
|
+
catch (parseError) {
|
|
236
|
+
// Protocol drift (version skew, non-SSE middlebox), NOT transport trouble:
|
|
237
|
+
// re-sending the same prompt cannot fix an unparseable stream — retryable: false.
|
|
238
|
+
// (Guarded by terminalSeen: garbage AFTER the terminal must not add a second one.)
|
|
239
|
+
if (!terminalSeen) {
|
|
240
|
+
yield {
|
|
241
|
+
type: "failed",
|
|
242
|
+
details: `remote invoke: unparseable event on the stream (${String(parseError)})`,
|
|
243
|
+
retryable: false,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Shape check, same discipline as the events plane: `data: null` / `data: 42` is
|
|
249
|
+
// valid JSON but protocol drift — it must not TypeError into the catch below and be
|
|
250
|
+
// misclassified as retryable network trouble.
|
|
251
|
+
if (typeof event !== "object" || event === null || typeof event.type !== "string") {
|
|
252
|
+
if (!terminalSeen) {
|
|
253
|
+
yield {
|
|
254
|
+
type: "failed",
|
|
255
|
+
details: "remote invoke: non-event data on the stream — protocol mismatch?",
|
|
256
|
+
retryable: false,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (event.type === "completed" || event.type === "failed")
|
|
262
|
+
terminalSeen = true;
|
|
263
|
+
yield event;
|
|
264
|
+
}
|
|
265
|
+
if (!terminalSeen) {
|
|
266
|
+
yield { type: "failed", details: "remote invoke: stream ended without a terminal", retryable: true };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (abort.signal.aborted) {
|
|
271
|
+
if (watchdog.stale() && !terminalSeen) {
|
|
272
|
+
yield {
|
|
273
|
+
type: "failed",
|
|
274
|
+
details: `remote invoke: no bytes for ${SSE_IDLE_LIMIT_MS / 1000}s (heartbeats absent) — dead connection`,
|
|
275
|
+
retryable: true,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return; // the consumer walked away — cancellation, not an error
|
|
279
|
+
}
|
|
280
|
+
if (!terminalSeen)
|
|
281
|
+
yield toFailed(error);
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
watchdog.stop();
|
|
285
|
+
}
|
|
286
|
+
})();
|
|
287
|
+
// ONE stream per invoke, like a local async generator (which is its own iterator): a second
|
|
288
|
+
// iteration must never re-POST — that would silently start a second run with the same prompt.
|
|
289
|
+
// Abort-first cancellation (see abortFirstIterator): disconnect = cancel the run, even
|
|
290
|
+
// while suspended on a quiet read.
|
|
291
|
+
const iterator = abortFirstIterator(openStream(), () => abort.abort());
|
|
292
|
+
return {
|
|
293
|
+
[Symbol.asyncIterator]() {
|
|
294
|
+
return iterator;
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** Minimal SSE reader: yields each `data:` payload; ignores comments (heartbeats) and other
|
|
301
|
+
* fields. The explicit reader loop (not for-await) exists for the watchdog: armed strictly
|
|
302
|
+
* around each pending read, so only "we are listening and nothing arrives" counts as idle — a
|
|
303
|
+
* consumer pausing at a yield leaves the watch disarmed (see {@link ReadWatch}). */
|
|
304
|
+
async function* sseData(body, watch) {
|
|
305
|
+
const decoder = new TextDecoder();
|
|
306
|
+
let buffer = "";
|
|
307
|
+
const reader = body.getReader();
|
|
308
|
+
try {
|
|
309
|
+
for (;;) {
|
|
310
|
+
watch?.arm(); // a read is pending — the idle clock may run
|
|
311
|
+
const { done, value } = await reader.read();
|
|
312
|
+
watch?.disarm(); // bytes (ANY bytes — heartbeats included) or a clean end arrived
|
|
313
|
+
if (done)
|
|
314
|
+
return;
|
|
315
|
+
// SSE permits CRLF line endings (proxies/other servers may produce them); normalize AFTER
|
|
316
|
+
// appending so a \r\n split across chunks still collapses once its second half arrives.
|
|
317
|
+
buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
|
|
318
|
+
let sep = buffer.indexOf("\n\n");
|
|
319
|
+
while (sep !== -1) {
|
|
320
|
+
const block = buffer.slice(0, sep);
|
|
321
|
+
buffer = buffer.slice(sep + 2);
|
|
322
|
+
const data = block
|
|
323
|
+
.split("\n")
|
|
324
|
+
.filter((line) => line.startsWith("data:"))
|
|
325
|
+
.map((line) => line.slice(5).trimStart())
|
|
326
|
+
.join("\n");
|
|
327
|
+
if (data !== "")
|
|
328
|
+
yield data;
|
|
329
|
+
sep = buffer.indexOf("\n\n");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
reader.releaseLock();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session control plane — the engine-neutral serving extension beside Agent Handler
|
|
3
|
+
* (docs/design/session-control.md). Pure types, zero dependencies; importing any engine
|
|
4
|
+
* implementation here is forbidden, exactly like agent.ts.
|
|
5
|
+
*
|
|
6
|
+
* The plane model: `invoke` is the only data plane (no run exists without an invoke); `dispatch`
|
|
7
|
+
* modulates the run an invoke drives; `state`/`entries`/`events` observe, strictly read-only.
|
|
8
|
+
*/
|
|
9
|
+
import type { Json, Prompt } from "./agent.ts";
|
|
10
|
+
export interface SessionControl {
|
|
11
|
+
capabilities(): SessionCapabilities;
|
|
12
|
+
state(session: string): Promise<SessionState>;
|
|
13
|
+
/** `since` is an APPEND-ORDER position cursor: "every record appended after the one with this
|
|
14
|
+
* id", regardless of branch structure. Reconstructing the active path in a branched session is
|
|
15
|
+
* the client's job via `parentId` chains from `leafEntryId`. An unknown cursor falls back to a
|
|
16
|
+
* full backfill (correct, merely larger). */
|
|
17
|
+
entries(session: string, options?: {
|
|
18
|
+
since?: string;
|
|
19
|
+
}): Promise<SessionEntries>;
|
|
20
|
+
events(session: string): AsyncIterable<SessionEvent>;
|
|
21
|
+
dispatch(session: string, command: SessionCommand): Promise<SessionResult>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Static support declaration, two kinds of flag:
|
|
25
|
+
* - COMMAND GATES (`steering`, `followUp`, `manualCompaction`, `modelSelection`, `thinkingLevel`):
|
|
26
|
+
* clients MUST gate dispatch on them; an unsupported command is rejected before acceptance with
|
|
27
|
+
* {@link UNSUPPORTED_CAPABILITY_CODE}.
|
|
28
|
+
* - OBSERVATION-QUALITY flags (`toolProgress`, `usage`): whether those events/state fields appear
|
|
29
|
+
* at all — nothing to dispatch, nothing to reject.
|
|
30
|
+
* `state`/`entries`/`events` are mandatory (the reconnect contract) and deliberately absent here.
|
|
31
|
+
*/
|
|
32
|
+
export interface SessionCapabilities {
|
|
33
|
+
steering: boolean;
|
|
34
|
+
followUp: boolean;
|
|
35
|
+
manualCompaction: boolean;
|
|
36
|
+
modelSelection: false | {
|
|
37
|
+
allowedModels: string[];
|
|
38
|
+
};
|
|
39
|
+
thinkingLevel: false | {
|
|
40
|
+
allowedLevels: string[];
|
|
41
|
+
};
|
|
42
|
+
toolProgress: boolean;
|
|
43
|
+
usage: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** Stable `SessionResult.error.code` for a command the implementation does not support. */
|
|
46
|
+
export declare const UNSUPPORTED_CAPABILITY_CODE = "unsupported_capability";
|
|
47
|
+
/** Stable `SessionResult.error.code` for a run-modulating command (`steer`/`follow_up`/`abort`)
|
|
48
|
+
* dispatched while the session has no active run — and, for `abort`, no in-flight manual
|
|
49
|
+
* compaction either (`abort` is also the door out of a `compacting` state; the outcome then
|
|
50
|
+
* travels as `compaction_finished{aborted}`). `retryable: false` — re-dispatching as-is fails
|
|
51
|
+
* again; re-dispatch only after `state()` shows an active run. */
|
|
52
|
+
export declare const NO_ACTIVE_RUN_CODE = "no_active_run";
|
|
53
|
+
/** Stable `SessionResult.error.code` for a command whose PAYLOAD is invalid for this runtime — an
|
|
54
|
+
* unknown model spec, an unsupported thinking level. Permanent for that payload; a different value
|
|
55
|
+
* may succeed. Rejected before acceptance. */
|
|
56
|
+
export declare const INVALID_COMMAND_CODE = "invalid_command";
|
|
57
|
+
/** Stable `SessionResult.error.code` for a boundary mutation on a session that does not exist.
|
|
58
|
+
* Sessions are created by the DATA plane (`invoke`), never by the control plane — a mutation on an
|
|
59
|
+
* unknown id (a typo, a not-yet-started conversation) must not mint a ghost record. Rejected
|
|
60
|
+
* before acceptance; retry after the session's first turn exists. */
|
|
61
|
+
export declare const NO_SUCH_SESSION_CODE = "no_such_session";
|
|
62
|
+
/** Stable `SessionResult.error.code` for a boundary mutation rejected BEFORE acceptance with
|
|
63
|
+
* nothing durable landed — a failed override append, or compact's admission failing (the harness
|
|
64
|
+
* build, the local preparation). Acceptance sits where the work becomes asynchronous and
|
|
65
|
+
* expensive: the model call — compact is accept-fast (holding the dispatch open for a full model
|
|
66
|
+
* call would make acceptance = outcome), and post-acceptance outcomes travel as
|
|
67
|
+
* `compaction_finished{summary|error|aborted}` events. `retryable: true` throughout: the same
|
|
68
|
+
* command may succeed on retry (the state-dependent "nothing to compact" has its own code,
|
|
69
|
+
* {@link NOTHING_TO_COMPACT_CODE}). */
|
|
70
|
+
export declare const BOUNDARY_COMMAND_FAILED_CODE = "boundary_command_failed";
|
|
71
|
+
/** Stable `SessionResult.error.code` for `compact` on a session with no compactable history yet —
|
|
72
|
+
* a no-op, not a failure, rejected before acceptance. The {@link NO_ACTIVE_RUN_CODE} pattern:
|
|
73
|
+
* `retryable: false` (as-is retry fails now), re-dispatch once the session has grown. */
|
|
74
|
+
export declare const NOTHING_TO_COMPACT_CODE = "nothing_to_compact";
|
|
75
|
+
/** Stable `SessionResult.error.code` for a run command that reached an active run but could not
|
|
76
|
+
* take effect because the run raced to settlement (or the engine refused it). Distinct from
|
|
77
|
+
* {@link NO_ACTIVE_RUN_CODE}: the run existed — and TRANSIENT: the session's next run can be
|
|
78
|
+
* dispatched to. Still pre-acceptance — nothing was queued — and `retryable: false`: the same
|
|
79
|
+
* command as-is fails again. (A run registered without modulation controls is a capability
|
|
80
|
+
* problem, not a run problem, and rejects with {@link UNSUPPORTED_CAPABILITY_CODE}.) */
|
|
81
|
+
export declare const RUN_COMMAND_FAILED_CODE = "run_command_failed";
|
|
82
|
+
/** Six commands; deliberately NO `prompt` — starting work is the data plane's definition. */
|
|
83
|
+
export type SessionCommand = {
|
|
84
|
+
type: "steer";
|
|
85
|
+
prompt: Prompt;
|
|
86
|
+
} | {
|
|
87
|
+
type: "follow_up";
|
|
88
|
+
prompt: Prompt;
|
|
89
|
+
} | {
|
|
90
|
+
type: "abort";
|
|
91
|
+
} | {
|
|
92
|
+
type: "compact";
|
|
93
|
+
instructions?: string;
|
|
94
|
+
} | {
|
|
95
|
+
type: "set_model";
|
|
96
|
+
model: string;
|
|
97
|
+
} | {
|
|
98
|
+
type: "set_thinking";
|
|
99
|
+
level: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Acceptance is not outcome: `ok: true` means admitted or applied, never that the run ultimately
|
|
103
|
+
* succeeded (outcomes are `run_settled` events / the invoke terminal). `ok: false` is guaranteed to
|
|
104
|
+
* mean rejection BEFORE acceptance — nothing was queued or applied. ONE exception to "nothing took
|
|
105
|
+
* effect": a rejected `abort` may still have attributed a concurrently-settling run as `aborted`
|
|
106
|
+
* (the intent was live while the run resolved — see the guarantee boundary in the pi
|
|
107
|
+
* implementation); the settlement is the truth. `error.retryable` means "re-dispatching the SAME
|
|
108
|
+
* command as-is may succeed"; a `false` with a state-dependent code (e.g.
|
|
109
|
+
* {@link NO_ACTIVE_RUN_CODE}) invites a re-dispatch only after `state()` shows the condition
|
|
110
|
+
* changed.
|
|
111
|
+
*/
|
|
112
|
+
export type SessionResult = {
|
|
113
|
+
ok: true;
|
|
114
|
+
runId?: string;
|
|
115
|
+
} | {
|
|
116
|
+
ok: false;
|
|
117
|
+
error: {
|
|
118
|
+
code: string;
|
|
119
|
+
message: string;
|
|
120
|
+
retryable: boolean;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
export interface SessionState {
|
|
124
|
+
/** `compacting` refers to Phase 2 MANUAL compaction at a session boundary. Automatic overflow
|
|
125
|
+
* compaction happens inside a run's activity window and reports as `running`. */
|
|
126
|
+
status: "idle" | "running" | "compacting";
|
|
127
|
+
activeRunId?: string;
|
|
128
|
+
/** The session's durable overrides (set_model / set_thinking), read from the record — so a
|
|
129
|
+
* reconnecting client sees them without scanning entries. Absent = the assembly default. */
|
|
130
|
+
model?: string;
|
|
131
|
+
thinkingLevel?: string;
|
|
132
|
+
pending: {
|
|
133
|
+
steering: number;
|
|
134
|
+
followUp: number;
|
|
135
|
+
};
|
|
136
|
+
usage?: {
|
|
137
|
+
inputTokens: number;
|
|
138
|
+
outputTokens: number;
|
|
139
|
+
cacheReadTokens?: number;
|
|
140
|
+
cacheWriteTokens?: number;
|
|
141
|
+
cost?: number;
|
|
142
|
+
contextTokens?: number;
|
|
143
|
+
contextWindow?: number;
|
|
144
|
+
};
|
|
145
|
+
leafEntryId?: string;
|
|
146
|
+
}
|
|
147
|
+
export interface SessionEntries {
|
|
148
|
+
entries: SessionEntry[];
|
|
149
|
+
leafEntryId?: string;
|
|
150
|
+
}
|
|
151
|
+
/** A durable append-only session record. `kind` guarantees a minimum vocabulary of
|
|
152
|
+
* "user" | "assistant" | "tool"; engine-specific kinds beyond it MUST be skippable. */
|
|
153
|
+
export interface SessionEntry {
|
|
154
|
+
id: string;
|
|
155
|
+
parentId?: string;
|
|
156
|
+
timestamp: number;
|
|
157
|
+
kind: string;
|
|
158
|
+
data: Json;
|
|
159
|
+
}
|
|
160
|
+
/** Semantic-only: no sequence, no epoch, no session id — in-process the stream is lossless and
|
|
161
|
+
* ordered, and those concerns belong to the transport envelope (design §13). Consumers MUST
|
|
162
|
+
* forward or ignore unknown event types; the vocabulary is additive. */
|
|
163
|
+
export interface SessionEvent<TType extends string = string, TData extends Json = Json> {
|
|
164
|
+
type: TType;
|
|
165
|
+
timestamp: number;
|
|
166
|
+
/** Present on run-scoped events. */
|
|
167
|
+
runId?: string;
|
|
168
|
+
data: TData;
|
|
169
|
+
}
|
|
170
|
+
export type RunStartedEvent = SessionEvent<"run_started", Record<never, never>> & {
|
|
171
|
+
runId: string;
|
|
172
|
+
};
|
|
173
|
+
export type RunSettledEvent = SessionEvent<"run_settled", {
|
|
174
|
+
status: "completed" | "failed" | "aborted";
|
|
175
|
+
error?: {
|
|
176
|
+
code?: string;
|
|
177
|
+
message: string;
|
|
178
|
+
retryable: boolean;
|
|
179
|
+
};
|
|
180
|
+
}> & {
|
|
181
|
+
runId: string;
|
|
182
|
+
};
|
|
183
|
+
export type MessageStartedEvent = SessionEvent<"message_started", Record<never, never>> & {
|
|
184
|
+
runId: string;
|
|
185
|
+
};
|
|
186
|
+
export type MessageDeltaEvent = SessionEvent<"message_delta", {
|
|
187
|
+
channel: "text" | "thinking";
|
|
188
|
+
delta: string;
|
|
189
|
+
}> & {
|
|
190
|
+
runId: string;
|
|
191
|
+
};
|
|
192
|
+
export type MessageFinishedEvent = SessionEvent<"message_finished", Record<never, never>> & {
|
|
193
|
+
runId: string;
|
|
194
|
+
};
|
|
195
|
+
export type ToolStartedEvent = SessionEvent<"tool_started", {
|
|
196
|
+
id: string;
|
|
197
|
+
name: string;
|
|
198
|
+
args: Json;
|
|
199
|
+
}> & {
|
|
200
|
+
runId: string;
|
|
201
|
+
};
|
|
202
|
+
/** Replace semantics: `partialResult` is the accumulated snapshot so far, not a delta. */
|
|
203
|
+
export type ToolProgressEvent = SessionEvent<"tool_progress", {
|
|
204
|
+
id: string;
|
|
205
|
+
name: string;
|
|
206
|
+
partialResult: Json;
|
|
207
|
+
}> & {
|
|
208
|
+
runId: string;
|
|
209
|
+
};
|
|
210
|
+
export type ToolFinishedEvent = SessionEvent<"tool_finished", {
|
|
211
|
+
id: string;
|
|
212
|
+
isError: boolean;
|
|
213
|
+
content: Json;
|
|
214
|
+
}> & {
|
|
215
|
+
runId: string;
|
|
216
|
+
};
|
|
217
|
+
/** Normalized live queue depths for the active run (L1). */
|
|
218
|
+
export type QueueChangedEvent = SessionEvent<"queue_changed", {
|
|
219
|
+
steering: number;
|
|
220
|
+
followUp: number;
|
|
221
|
+
}> & {
|
|
222
|
+
runId: string;
|
|
223
|
+
};
|
|
224
|
+
/** A boundary mutation changed durable session state (L2; no runId — boundary mutations happen
|
|
225
|
+
* between runs). */
|
|
226
|
+
export type StateChangedEvent = SessionEvent<"state_changed", {
|
|
227
|
+
model?: string;
|
|
228
|
+
thinkingLevel?: string;
|
|
229
|
+
}>;
|
|
230
|
+
/** Manual compaction bounds (L2): every `compaction_started` is closed by exactly one
|
|
231
|
+
* `compaction_finished` — `summary` on success, `error` on failure, `aborted: true` on a
|
|
232
|
+
* deliberate stop (run/compaction symmetry with `run_settled{status: "aborted"}`: a client's own
|
|
233
|
+
* abort is not a failure). In the failure and aborted cases nothing durable landed. Automatic
|
|
234
|
+
* overflow compaction stays inside its run's activity window and does not emit these. */
|
|
235
|
+
export type CompactionStartedEvent = SessionEvent<"compaction_started", Record<never, never>>;
|
|
236
|
+
export type CompactionFinishedEvent = SessionEvent<"compaction_finished", {
|
|
237
|
+
summary?: string;
|
|
238
|
+
error?: string;
|
|
239
|
+
aborted?: boolean;
|
|
240
|
+
}>;
|
|
241
|
+
/** A transient provider failure scheduled a summarization retry backoff (auto-compaction /
|
|
242
|
+
* branch summaries inside a run — `runId` present — or a manual `compact` at a boundary — no
|
|
243
|
+
* `runId`). Explains a quiet gap that would otherwise read as a hang. Deliberately unclosed:
|
|
244
|
+
* the next event (message_*, `run_settled`, `compaction_finished`) is the closure, and the
|
|
245
|
+
* engine's `retry_finished` carries no outcome to forward. */
|
|
246
|
+
export type RetryScheduledEvent = SessionEvent<"retry_scheduled", {
|
|
247
|
+
operation: "compaction" | "branch_summary";
|
|
248
|
+
attempt: number;
|
|
249
|
+
maxAttempts: number;
|
|
250
|
+
delayMs: number;
|
|
251
|
+
error: string;
|
|
252
|
+
}>;
|
|
253
|
+
/**
|
|
254
|
+
* The serving process failed outside a normal run outcome (fail visibly). Emitted by TRANSPORT
|
|
255
|
+
* adapters (design §13) when they lose the backend before ending a remote stream — an in-process
|
|
256
|
+
* embedding cannot produce it (a dead process has no one left to emit), so it is deliberately NOT
|
|
257
|
+
* part of {@link KnownSessionEvent}: a local L0 client would be handling a signal that cannot occur.
|
|
258
|
+
*/
|
|
259
|
+
export type ServingErrorEvent = SessionEvent<"serving_error", {
|
|
260
|
+
message: string;
|
|
261
|
+
}>;
|
|
262
|
+
/** Every event the in-process observation plane emits today: L0, L1 `queue_changed`, and the L2
|
|
263
|
+
* events (`state_changed`, `compaction_*`, `retry_scheduled`). Remaining L2 events (turn_*) are
|
|
264
|
+
* future vocabulary; {@link ServingErrorEvent} arrives with the transport adapter. */
|
|
265
|
+
export type KnownSessionEvent = RunStartedEvent | RunSettledEvent | MessageStartedEvent | MessageDeltaEvent | MessageFinishedEvent | ToolStartedEvent | ToolProgressEvent | ToolFinishedEvent | QueueChangedEvent | StateChangedEvent | CompactionStartedEvent | CompactionFinishedEvent | RetryScheduledEvent;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Stable `SessionResult.error.code` for a command the implementation does not support. */
|
|
2
|
+
export const UNSUPPORTED_CAPABILITY_CODE = "unsupported_capability";
|
|
3
|
+
/** Stable `SessionResult.error.code` for a run-modulating command (`steer`/`follow_up`/`abort`)
|
|
4
|
+
* dispatched while the session has no active run — and, for `abort`, no in-flight manual
|
|
5
|
+
* compaction either (`abort` is also the door out of a `compacting` state; the outcome then
|
|
6
|
+
* travels as `compaction_finished{aborted}`). `retryable: false` — re-dispatching as-is fails
|
|
7
|
+
* again; re-dispatch only after `state()` shows an active run. */
|
|
8
|
+
export const NO_ACTIVE_RUN_CODE = "no_active_run";
|
|
9
|
+
/** Stable `SessionResult.error.code` for a command whose PAYLOAD is invalid for this runtime — an
|
|
10
|
+
* unknown model spec, an unsupported thinking level. Permanent for that payload; a different value
|
|
11
|
+
* may succeed. Rejected before acceptance. */
|
|
12
|
+
export const INVALID_COMMAND_CODE = "invalid_command";
|
|
13
|
+
/** Stable `SessionResult.error.code` for a boundary mutation on a session that does not exist.
|
|
14
|
+
* Sessions are created by the DATA plane (`invoke`), never by the control plane — a mutation on an
|
|
15
|
+
* unknown id (a typo, a not-yet-started conversation) must not mint a ghost record. Rejected
|
|
16
|
+
* before acceptance; retry after the session's first turn exists. */
|
|
17
|
+
export const NO_SUCH_SESSION_CODE = "no_such_session";
|
|
18
|
+
/** Stable `SessionResult.error.code` for a boundary mutation rejected BEFORE acceptance with
|
|
19
|
+
* nothing durable landed — a failed override append, or compact's admission failing (the harness
|
|
20
|
+
* build, the local preparation). Acceptance sits where the work becomes asynchronous and
|
|
21
|
+
* expensive: the model call — compact is accept-fast (holding the dispatch open for a full model
|
|
22
|
+
* call would make acceptance = outcome), and post-acceptance outcomes travel as
|
|
23
|
+
* `compaction_finished{summary|error|aborted}` events. `retryable: true` throughout: the same
|
|
24
|
+
* command may succeed on retry (the state-dependent "nothing to compact" has its own code,
|
|
25
|
+
* {@link NOTHING_TO_COMPACT_CODE}). */
|
|
26
|
+
export const BOUNDARY_COMMAND_FAILED_CODE = "boundary_command_failed";
|
|
27
|
+
/** Stable `SessionResult.error.code` for `compact` on a session with no compactable history yet —
|
|
28
|
+
* a no-op, not a failure, rejected before acceptance. The {@link NO_ACTIVE_RUN_CODE} pattern:
|
|
29
|
+
* `retryable: false` (as-is retry fails now), re-dispatch once the session has grown. */
|
|
30
|
+
export const NOTHING_TO_COMPACT_CODE = "nothing_to_compact";
|
|
31
|
+
/** Stable `SessionResult.error.code` for a run command that reached an active run but could not
|
|
32
|
+
* take effect because the run raced to settlement (or the engine refused it). Distinct from
|
|
33
|
+
* {@link NO_ACTIVE_RUN_CODE}: the run existed — and TRANSIENT: the session's next run can be
|
|
34
|
+
* dispatched to. Still pre-acceptance — nothing was queued — and `retryable: false`: the same
|
|
35
|
+
* command as-is fails again. (A run registered without modulation controls is a capability
|
|
36
|
+
* problem, not a run problem, and rejects with {@link UNSUPPORTED_CAPABILITY_CODE}.) */
|
|
37
|
+
export const RUN_COMMAND_FAILED_CODE = "run_command_failed";
|
package/dist/slack.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** `@fastagent-sh/fastagent/slack` — the first-party Slack Events API bot-channel surface. */
|
|
2
|
+
export { slackChannel, defaultSlackRoute, slackEnvelope, verifySlackSignature, type SlackChannelOptions, type SlackEventEnvelope, type SlackFile, type SlackMessageEvent, type SlackRendering, type SlackRoute, type SlackFailure, } from "./channels/slack/slack.ts";
|
package/dist/slack.js
ADDED