@cursor/july 0.1.7 → 0.1.8
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/dist/bin/agent-serve.js +31 -2
- package/dist/channels/slack/channel-watch.d.ts +6 -1
- package/dist/channels/slack/channel-watch.d.ts.map +1 -1
- package/dist/channels/slack/channel-watch.js +22 -1
- package/dist/channels/slack/cursor-account.d.ts +4 -0
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +12 -2
- package/dist/channels/slack/defaults.d.ts +10 -5
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +39 -17
- package/dist/channels/slack/dispatch.d.ts +18 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +141 -38
- package/dist/channels/slack/inbound.d.ts +13 -3
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +17 -9
- package/dist/channels/slack/index.d.ts +4 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +4 -0
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +1 -0
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +3 -0
- package/dist/channels/slack/nudge-carrier.d.ts +19 -0
- package/dist/channels/slack/nudge-carrier.d.ts.map +1 -0
- package/dist/channels/slack/nudge-carrier.js +166 -0
- package/dist/channels/slack/nudge-store.d.ts +61 -0
- package/dist/channels/slack/nudge-store.d.ts.map +1 -0
- package/dist/channels/slack/nudge-store.js +266 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +74 -25
- package/dist/channels/slack/stop.d.ts +14 -0
- package/dist/channels/slack/stop.d.ts.map +1 -0
- package/dist/channels/slack/stop.js +22 -0
- package/dist/channels/slack/thread-context.d.ts +81 -0
- package/dist/channels/slack/thread-context.d.ts.map +1 -0
- package/dist/channels/slack/thread-context.js +470 -0
- package/dist/channels/slack/types.d.ts +27 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.COTN7wgo.js → app.BP8mAVMW.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DevOabKJ.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BW3TBdT0.js → VPLocalSearchBox.1FXIZbz8.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BEJW0vE7.js → theme.Dm1R02v9.js} +2 -2
- package/dist/docs/assets/example-agents_index.md.CwtWu5ix.js +2 -0
- package/dist/docs/assets/{example-agents_index.md.CRqJlnIf.lean.js → example-agents_index.md.CwtWu5ix.lean.js} +1 -1
- package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.js +10 -0
- package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.lean.js +1 -0
- package/dist/docs/assets/{guides_slack.md.CCwqHvSV.js → guides_slack.md.FWale6Ip.js} +4 -3
- package/dist/docs/assets/{guides_slack.md.CCwqHvSV.lean.js → guides_slack.md.FWale6Ip.lean.js} +1 -1
- package/dist/docs/assets/{reference_sessions.md.DdjWabnN.js → reference_sessions.md.DLd6mvbv.js} +2 -2
- package/dist/docs/assets/{reference_sessions.md.DdjWabnN.lean.js → reference_sessions.md.DLd6mvbv.lean.js} +1 -1
- package/dist/docs/assets/{storage.md.CVnInNiN.js → storage.md.Cu3MwM2a.js} +3 -2
- package/dist/docs/assets/{storage.md.CVnInNiN.lean.js → storage.md.Cu3MwM2a.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +4 -4
- package/dist/docs/evals.html +4 -4
- package/dist/docs/example-agents/approval-buddy.html +4 -4
- package/dist/docs/example-agents/benny.html +5 -5
- package/dist/docs/example-agents/bugbot.html +5 -5
- package/dist/docs/example-agents/codebase-wiki.html +4 -4
- package/dist/docs/example-agents/codeowners-review.html +4 -4
- package/dist/docs/example-agents/concierge.html +4 -4
- package/dist/docs/example-agents/fsd.html +4 -4
- package/dist/docs/example-agents/index.html +6 -6
- package/dist/docs/example-agents/knowledge-base.html +4 -4
- package/dist/docs/example-agents/oncall.html +34 -0
- package/dist/docs/example-agents/security-reviewer.html +4 -4
- package/dist/docs/example-agents/slack-agent.html +4 -4
- package/dist/docs/example-agents/weather-agent.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/slack.html +7 -6
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +4 -4
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +4 -4
- package/dist/docs/reference/connections.html +4 -4
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +5 -5
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +4 -4
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +7 -6
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/internal/cli-mcp.d.ts +64 -0
- package/dist/internal/cli-mcp.d.ts.map +1 -0
- package/dist/internal/cli-mcp.js +570 -0
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +2 -0
- package/dist/internal/host-kv.d.ts +17 -0
- package/dist/internal/host-kv.d.ts.map +1 -0
- package/dist/internal/host-kv.js +77 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +8 -0
- package/dist/internal/session-engine.d.ts +42 -7
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +247 -51
- package/dist/internal/storage-coordinator.d.ts +18 -1
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +43 -0
- package/dist/playground/assets/index-7GJIUFQj.css +1 -0
- package/dist/playground/assets/index-Bpr4Th4M.js +319 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/file-kv.d.ts +42 -0
- package/dist/storage-backends/file-kv.d.ts.map +1 -0
- package/dist/storage-backends/file-kv.js +190 -0
- package/dist/storage-backends/postgres-kv.d.ts +77 -0
- package/dist/storage-backends/postgres-kv.d.ts.map +1 -0
- package/dist/storage-backends/postgres-kv.js +180 -0
- package/dist/storage.d.ts +10 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +10 -1
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/example-agents/index.md +14 -9
- package/docs/example-agents/oncall.md +216 -0
- package/docs/guides/slack.md +11 -2
- package/docs/reference/sessions.md +13 -4
- package/docs/storage.md +27 -0
- package/package.json +15 -1
- package/skills/framework-map/SKILL.md +6 -4
- package/src/bin/agent-serve.ts +32 -2
- package/src/channels/slack/channel-watch.ts +28 -1
- package/src/channels/slack/cursor-account.ts +20 -0
- package/src/channels/slack/defaults.ts +51 -20
- package/src/channels/slack/dispatch.ts +215 -40
- package/src/channels/slack/inbound.ts +30 -10
- package/src/channels/slack/index.ts +4 -0
- package/src/channels/slack/live-delivery.ts +1 -0
- package/src/channels/slack/manifest.ts +3 -0
- package/src/channels/slack/nudge-carrier.ts +182 -0
- package/src/channels/slack/nudge-store.ts +308 -0
- package/src/channels/slack/slack-channel.ts +106 -31
- package/src/channels/slack/stop.ts +25 -0
- package/src/channels/slack/thread-context.ts +572 -0
- package/src/channels/slack/types.ts +27 -0
- package/src/internal/cli-mcp.ts +708 -0
- package/src/internal/distribution.ts +2 -0
- package/src/internal/host-kv.ts +85 -0
- package/src/internal/server.ts +31 -0
- package/src/internal/session-engine.ts +351 -50
- package/src/internal/storage-coordinator.ts +41 -0
- package/src/storage-backends/file-kv.ts +221 -0
- package/src/storage-backends/postgres-kv.ts +248 -0
- package/src/storage.ts +11 -1
- package/src/types.ts +83 -0
- package/dist/channels/slack/post-update-delivery.d.ts +0 -85
- package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.B7UcKvIn.js +0 -1
- package/dist/docs/assets/example-agents_index.md.CRqJlnIf.js +0 -2
- package/dist/internal/json-dir-store.d.ts +0 -32
- package/dist/internal/json-dir-store.d.ts.map +0 -1
- package/dist/internal/persistence-coordinator.d.ts +0 -127
- package/dist/internal/persistence-coordinator.d.ts.map +0 -1
- package/dist/persistence.d.ts +0 -184
- package/dist/persistence.d.ts.map +0 -1
- package/dist/playground/assets/index-72vCOBWO.js +0 -86
- package/dist/playground/assets/index-BjnMwYoR.css +0 -1
|
@@ -392,6 +392,47 @@ export class StorageCoordinator {
|
|
|
392
392
|
);
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
// ==========================================================================
|
|
396
|
+
// Author KV ({@link HostContext.kv})
|
|
397
|
+
// ==========================================================================
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Whether the sink can serve awaited author reads/writes (`get` is
|
|
401
|
+
* required — `put` is always present on {@link StorageConfig}).
|
|
402
|
+
*/
|
|
403
|
+
get supportsAuthorKv(): boolean {
|
|
404
|
+
return this.definition.get !== undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Awaited author KV read. Unlike framework session mirrors, this awaits
|
|
409
|
+
* the sink and propagates errors — handlers need read-after-write.
|
|
410
|
+
*/
|
|
411
|
+
async kvGet(authorKey: string): Promise<JsonValue | undefined> {
|
|
412
|
+
const get = this.definition.get;
|
|
413
|
+
if (get === undefined) {
|
|
414
|
+
throw new Error("storage get is not configured");
|
|
415
|
+
}
|
|
416
|
+
const key = storageKeys.kv(this.agentName, authorKey);
|
|
417
|
+
// Preserve stored JSON `null` (distinct from a missing key).
|
|
418
|
+
return await get(key, this.context("restore"));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Awaited author KV write. Goes straight to the sink (not the
|
|
423
|
+
* at-most-once delivery queue) so callers observe durable success/failure.
|
|
424
|
+
*/
|
|
425
|
+
async kvPut(authorKey: string, value: JsonValue): Promise<void> {
|
|
426
|
+
const key = storageKeys.kv(this.agentName, authorKey);
|
|
427
|
+
await this.definition.put(key, value, this.context("policy"));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Awaited author KV delete. No-op when the sink omits `delete`. */
|
|
431
|
+
async kvDelete(authorKey: string): Promise<void> {
|
|
432
|
+
const key = storageKeys.kv(this.agentName, authorKey);
|
|
433
|
+
await this.definition.delete?.(key, this.context("policy"));
|
|
434
|
+
}
|
|
435
|
+
|
|
395
436
|
// ==========================================================================
|
|
396
437
|
// Evals (dedicated `evals` table)
|
|
397
438
|
// ==========================================================================
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed `defineStorage` helper: a file-per-key KV store plus
|
|
3
|
+
* file-backed `evals` / `abs` tables.
|
|
4
|
+
*
|
|
5
|
+
* Implements the whole `defineStorage` surface with zero external
|
|
6
|
+
* dependencies: the four KV methods against a flat directory of JSON files
|
|
7
|
+
* (one file per key, filename = URI-encoded key), eval runs as
|
|
8
|
+
* `eval-runs/<agent>/<runId>.json`, and A/B metrics as
|
|
9
|
+
* `ab/<agent>/samples.ndjson` (append-only) plus `ab/<agent>/snapshot.json`
|
|
10
|
+
* (latest aggregate). KV keys carry the agent name inside the framework
|
|
11
|
+
* key scheme, but the evals/abs tables have no such scheme — the agent
|
|
12
|
+
* subdirectory keeps agents sharing one storage `dir` from mixing or
|
|
13
|
+
* overwriting each other's history. Swap this for a real backend
|
|
14
|
+
* (Postgres, S3, …) in production; the contracts are identical.
|
|
15
|
+
*
|
|
16
|
+
* Layout notes:
|
|
17
|
+
* - KV filenames encode the full key (`encodeURIComponent(key) + ".json"`),
|
|
18
|
+
* so `list(prefix)` is a readdir + decode + filter, returned in key
|
|
19
|
+
* order as the contract requires.
|
|
20
|
+
* - `put` writes via tmp + rename: the framework serializes writes, but a
|
|
21
|
+
* restore-path `get`/`list` can run concurrently and must never see a
|
|
22
|
+
* torn file.
|
|
23
|
+
* - `get` distinguishes a definitive miss (ENOENT → `undefined`) from a
|
|
24
|
+
* real failure (anything else throws) — on the lazy-restore path a
|
|
25
|
+
* throw correctly fails the follow-up instead of forking the session.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { randomUUID } from "node:crypto";
|
|
29
|
+
import {
|
|
30
|
+
appendFile,
|
|
31
|
+
mkdir,
|
|
32
|
+
readdir,
|
|
33
|
+
readFile,
|
|
34
|
+
rename,
|
|
35
|
+
rm,
|
|
36
|
+
writeFile,
|
|
37
|
+
} from "node:fs/promises";
|
|
38
|
+
import { join } from "node:path";
|
|
39
|
+
import type { ABSnapshot } from "../ab.js";
|
|
40
|
+
import type { EvalRunSnapshot } from "../evals.js";
|
|
41
|
+
import type {
|
|
42
|
+
StorageABTable,
|
|
43
|
+
StorageConfig,
|
|
44
|
+
StorageContext,
|
|
45
|
+
StorageEntry,
|
|
46
|
+
StorageEvalsTable,
|
|
47
|
+
} from "../storage.js";
|
|
48
|
+
import type { JsonValue } from "../types.js";
|
|
49
|
+
|
|
50
|
+
export interface FileKvOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Directory holding the storage files, relative to the agent project root
|
|
53
|
+
* (default `".agent-serve/kv"`). Absolute paths are used as-is.
|
|
54
|
+
*/
|
|
55
|
+
dir?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type FileKvMethods = Required<
|
|
59
|
+
Pick<StorageConfig, "put" | "get" | "delete" | "list">
|
|
60
|
+
> & {
|
|
61
|
+
evals: StorageEvalsTable;
|
|
62
|
+
abs: StorageABTable;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** File-backed `defineStorage` methods, ready to spread into `defineStorage({...})`. */
|
|
66
|
+
export function fileKv(options?: FileKvOptions): FileKvMethods {
|
|
67
|
+
const resolveDir = (ctx: StorageContext): string =>
|
|
68
|
+
join(ctx.projectRoot, options?.dir ?? ".agent-serve/kv");
|
|
69
|
+
// The evals/abs tables are scoped per agent (KV keys already carry the
|
|
70
|
+
// agent name via the framework key scheme).
|
|
71
|
+
const evalRunsDir = (ctx: StorageContext): string =>
|
|
72
|
+
join(resolveDir(ctx), "eval-runs", encodeURIComponent(ctx.agentName));
|
|
73
|
+
const abDir = (ctx: StorageContext): string =>
|
|
74
|
+
join(resolveDir(ctx), "ab", encodeURIComponent(ctx.agentName));
|
|
75
|
+
const filePath = (ctx: StorageContext, key: string): string =>
|
|
76
|
+
join(resolveDir(ctx), `${encodeURIComponent(key)}.json`);
|
|
77
|
+
const readKey = async (
|
|
78
|
+
ctx: StorageContext,
|
|
79
|
+
key: string
|
|
80
|
+
): Promise<JsonValue | undefined> => {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(
|
|
83
|
+
await readFile(filePath(ctx, key), "utf8")
|
|
84
|
+
) as JsonValue;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
87
|
+
return undefined; // definitive miss — key was never written
|
|
88
|
+
}
|
|
89
|
+
throw error; // real failure: let lazy restore fail the follow-up
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
async put(key, value, ctx): Promise<void> {
|
|
95
|
+
await writeJsonAtomic({
|
|
96
|
+
dir: resolveDir(ctx),
|
|
97
|
+
fileName: `${encodeURIComponent(key)}.json`,
|
|
98
|
+
value,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
get(key, ctx): Promise<JsonValue | undefined> {
|
|
103
|
+
return readKey(ctx, key);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async delete(key, ctx): Promise<void> {
|
|
107
|
+
await rm(filePath(ctx, key), { force: true });
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async list(prefix, ctx): Promise<StorageEntry[]> {
|
|
111
|
+
let names: string[];
|
|
112
|
+
try {
|
|
113
|
+
names = await readdir(resolveDir(ctx));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
116
|
+
return []; // nothing stored yet
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
const keys = names
|
|
121
|
+
.filter((name) => name.endsWith(".json"))
|
|
122
|
+
.map((name) => decodeURIComponent(name.slice(0, -".json".length)))
|
|
123
|
+
.filter((key) => key.startsWith(prefix))
|
|
124
|
+
.sort();
|
|
125
|
+
const entries: StorageEntry[] = [];
|
|
126
|
+
for (const key of keys) {
|
|
127
|
+
const value = await readKey(ctx, key);
|
|
128
|
+
if (value !== undefined) {
|
|
129
|
+
entries.push({ key, value });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return entries;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// Playground eval batches: one JSON file per run under eval-runs/<agent>/.
|
|
136
|
+
evals: {
|
|
137
|
+
async put(run, ctx): Promise<void> {
|
|
138
|
+
await writeJsonAtomic({
|
|
139
|
+
dir: evalRunsDir(ctx),
|
|
140
|
+
fileName: `${encodeURIComponent(run.runId)}.json`,
|
|
141
|
+
value: run,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
async delete(runId, ctx): Promise<void> {
|
|
145
|
+
await rm(join(evalRunsDir(ctx), `${encodeURIComponent(runId)}.json`), {
|
|
146
|
+
force: true,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
async list(ctx): Promise<EvalRunSnapshot[]> {
|
|
150
|
+
const dir = evalRunsDir(ctx);
|
|
151
|
+
let names: string[];
|
|
152
|
+
try {
|
|
153
|
+
names = await readdir(dir);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
const runs: EvalRunSnapshot[] = [];
|
|
161
|
+
for (const name of names.filter((n) => n.endsWith(".json")).sort()) {
|
|
162
|
+
// One unreadable/corrupt file must not wipe the whole history:
|
|
163
|
+
// hydrate treats a throwing list() as "nothing saved".
|
|
164
|
+
try {
|
|
165
|
+
runs.push(
|
|
166
|
+
JSON.parse(
|
|
167
|
+
await readFile(join(dir, name), "utf8")
|
|
168
|
+
) as EvalRunSnapshot
|
|
169
|
+
);
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
return runs;
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// A/B metrics: append-only sample log + latest aggregate snapshot,
|
|
177
|
+
// both under ab/<agent>/.
|
|
178
|
+
abs: {
|
|
179
|
+
async putSample(sample, ctx): Promise<void> {
|
|
180
|
+
const dir = abDir(ctx);
|
|
181
|
+
await mkdir(dir, { recursive: true });
|
|
182
|
+
await appendFile(
|
|
183
|
+
join(dir, "samples.ndjson"),
|
|
184
|
+
`${JSON.stringify(sample)}\n`,
|
|
185
|
+
"utf8"
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
async putSnapshot(snapshot, ctx): Promise<void> {
|
|
189
|
+
await writeJsonAtomic({
|
|
190
|
+
dir: abDir(ctx),
|
|
191
|
+
fileName: "snapshot.json",
|
|
192
|
+
value: snapshot,
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
async getSnapshot(ctx): Promise<ABSnapshot | undefined> {
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(
|
|
198
|
+
await readFile(join(abDir(ctx), "snapshot.json"), "utf8")
|
|
199
|
+
) as ABSnapshot;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** tmp + rename write so concurrent readers never see a torn file. */
|
|
212
|
+
async function writeJsonAtomic(args: {
|
|
213
|
+
dir: string;
|
|
214
|
+
fileName: string;
|
|
215
|
+
value: unknown;
|
|
216
|
+
}): Promise<void> {
|
|
217
|
+
await mkdir(args.dir, { recursive: true });
|
|
218
|
+
const tmp = join(args.dir, `.tmp-${randomUUID()}`);
|
|
219
|
+
await writeFile(tmp, JSON.stringify(args.value), "utf8");
|
|
220
|
+
await rename(tmp, join(args.dir, args.fileName));
|
|
221
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres `defineStorage` helper on the agent-serve tables
|
|
3
|
+
* (backend/packages/prisma-bugbot):
|
|
4
|
+
*
|
|
5
|
+
* agent_serve_kv(deployment_id, key, value JSONB, updated_at)
|
|
6
|
+
* PRIMARY KEY (deployment_id, key)
|
|
7
|
+
* agent_serve_eval_runs(deployment_id, agent, run_id, run JSONB, updated_at)
|
|
8
|
+
* PRIMARY KEY (deployment_id, agent, run_id)
|
|
9
|
+
* agent_serve_ab(deployment_id, agent, kind, ref, value JSONB, updated_at)
|
|
10
|
+
* PRIMARY KEY (deployment_id, agent, kind, ref)
|
|
11
|
+
*
|
|
12
|
+
* `postgresStorage` implements the whole `defineStorage` surface as plain
|
|
13
|
+
* SQL: the four KV methods against the last-write-wins `agent_serve_kv`
|
|
14
|
+
* table (sessions, event chunks, continuations, reminders), the `evals`
|
|
15
|
+
* table against `agent_serve_eval_runs` (one row per playground eval
|
|
16
|
+
* batch), and the `abs` table against `agent_serve_ab` (`kind = 'sample'`
|
|
17
|
+
* rows per metric sample plus one `kind = 'snapshot'` row holding the
|
|
18
|
+
* latest aggregate).
|
|
19
|
+
*
|
|
20
|
+
* - KV `put` upserts on the primary key (LWW matches the framework contract).
|
|
21
|
+
* - KV `list` is a prefix range scan on the PK btree (`key >= p AND key < p||…`
|
|
22
|
+
* spelled as LIKE with escaped wildcards, ordered by key).
|
|
23
|
+
* - `get` returning no row is a definitive miss (`undefined`); a connection
|
|
24
|
+
* error throws, so lazy restore fails the follow-up instead of forking
|
|
25
|
+
* the session.
|
|
26
|
+
*
|
|
27
|
+
* Connection comes from `BUGBOTDB_URL` — the repo-wide convention for the
|
|
28
|
+
* bugbot Postgres database, whose Prisma package owns these tables' schema
|
|
29
|
+
* and migrations; locally it falls back to the dev bugbot DB. Non-local
|
|
30
|
+
* URLs enable TLS (`ssl.rejectUnauthorized`) because PlanetScale requires
|
|
31
|
+
* SSL even when the vault URL omits `sslmode`.
|
|
32
|
+
* `deploymentId` scopes rows per deployment (`AGENT_SERVE_DEPLOYMENT_ID`);
|
|
33
|
+
* the local default keeps dev runs of different checkouts from colliding
|
|
34
|
+
* only by agent name.
|
|
35
|
+
*
|
|
36
|
+
* Callers must pass `Pool` from their own `import { Pool } from "pg"`.
|
|
37
|
+
* This module must not import `pg` itself: Node resolves modules from
|
|
38
|
+
* *this* file's directory tree, so a `pg` dep declared only on the
|
|
39
|
+
* consuming agent package would never be found after a hosted install.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type { ABSnapshot } from "../ab.js";
|
|
43
|
+
import type { EvalRunSnapshot } from "../evals.js";
|
|
44
|
+
import type {
|
|
45
|
+
StorageABTable,
|
|
46
|
+
StorageConfig,
|
|
47
|
+
StorageEntry,
|
|
48
|
+
StorageEvalsTable,
|
|
49
|
+
} from "../storage.js";
|
|
50
|
+
import type { JsonValue } from "../types.js";
|
|
51
|
+
|
|
52
|
+
/** Minimal `pg.Pool` surface used here (avoids importing `pg` from this path). */
|
|
53
|
+
export interface PostgresQueryable {
|
|
54
|
+
query<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
55
|
+
text: string,
|
|
56
|
+
values?: readonly unknown[]
|
|
57
|
+
): Promise<{ rows: T[] }>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `pg.Pool` constructor supplied by the consuming example package. */
|
|
61
|
+
export type PostgresPoolConstructor = new (config?: {
|
|
62
|
+
connectionString?: string;
|
|
63
|
+
max?: number;
|
|
64
|
+
ssl?: boolean | { rejectUnauthorized?: boolean };
|
|
65
|
+
}) => PostgresQueryable;
|
|
66
|
+
|
|
67
|
+
export interface PostgresStorageOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Pass `Pool` from `import { Pool } from "pg"` in the example that declares
|
|
70
|
+
* the `pg` dependency (hosted installs put it in that package's
|
|
71
|
+
* `node_modules` only).
|
|
72
|
+
*/
|
|
73
|
+
Pool: PostgresPoolConstructor;
|
|
74
|
+
/** Postgres connection string (default: env, then local bugbot dev DB). */
|
|
75
|
+
databaseUrl?: string;
|
|
76
|
+
/** Value for the tables' deployment_id column (default: env or "local-dev"). */
|
|
77
|
+
deploymentId?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type PostgresStorageMethods = Required<
|
|
81
|
+
Pick<StorageConfig, "put" | "get" | "delete" | "list">
|
|
82
|
+
> & {
|
|
83
|
+
evals: StorageEvalsTable;
|
|
84
|
+
abs: StorageABTable;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** True for local bugbot/dev URLs that do not speak TLS. */
|
|
88
|
+
export function isLocalPostgresUrl(connectionString: string): boolean {
|
|
89
|
+
try {
|
|
90
|
+
const host = new URL(connectionString).hostname.toLowerCase();
|
|
91
|
+
return (
|
|
92
|
+
host === "localhost" ||
|
|
93
|
+
host === "127.0.0.1" ||
|
|
94
|
+
host === "::1" ||
|
|
95
|
+
host === "[::1]"
|
|
96
|
+
);
|
|
97
|
+
} catch {
|
|
98
|
+
return /@(localhost|127\.0\.0\.1|\[::1\])([:/]|$)/i.test(connectionString);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Postgres-backed `defineStorage` methods, ready to spread into `defineStorage({...})`. */
|
|
103
|
+
export function postgresStorage(
|
|
104
|
+
options: PostgresStorageOptions
|
|
105
|
+
): PostgresStorageMethods {
|
|
106
|
+
const connectionString =
|
|
107
|
+
options.databaseUrl ??
|
|
108
|
+
process.env.BUGBOTDB_URL ??
|
|
109
|
+
"postgresql://prisma:prisma@localhost:5432/bugbot";
|
|
110
|
+
// Prod Bugbot is PlanetScale Postgres: TLS is required and the vault
|
|
111
|
+
// `BUGBOTDB_URL` often omits `sslmode`. Without an explicit `ssl` option,
|
|
112
|
+
// `pg` opens plaintext and PlanetScale rejects with "SSL connection is
|
|
113
|
+
// required" — sessions stay in-memory. Skip TLS only for local hosts.
|
|
114
|
+
const pool = new options.Pool({
|
|
115
|
+
connectionString,
|
|
116
|
+
// Small fixed pool per agent, mirroring the backend's connection_limit
|
|
117
|
+
// clamp on BUGBOTDB_URL consumers (backend/server/src/prisma.ts).
|
|
118
|
+
max: 3,
|
|
119
|
+
...(isLocalPostgresUrl(connectionString)
|
|
120
|
+
? {}
|
|
121
|
+
: { ssl: { rejectUnauthorized: true } }),
|
|
122
|
+
});
|
|
123
|
+
const deploymentId =
|
|
124
|
+
options.deploymentId ??
|
|
125
|
+
process.env.AGENT_SERVE_DEPLOYMENT_ID ??
|
|
126
|
+
"local-dev";
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
async put(key, value): Promise<void> {
|
|
130
|
+
await pool.query(
|
|
131
|
+
`INSERT INTO agent_serve_kv (deployment_id, key, value, updated_at)
|
|
132
|
+
VALUES ($1, $2, $3::jsonb, now())
|
|
133
|
+
ON CONFLICT ON CONSTRAINT pk_agent_serve_kv
|
|
134
|
+
DO UPDATE SET value = EXCLUDED.value, updated_at = now()`,
|
|
135
|
+
[deploymentId, key, JSON.stringify(value)]
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
async get(key): Promise<JsonValue | undefined> {
|
|
140
|
+
const result = await pool.query<{ value: JsonValue }>(
|
|
141
|
+
`SELECT value FROM agent_serve_kv
|
|
142
|
+
WHERE deployment_id = $1 AND key = $2`,
|
|
143
|
+
[deploymentId, key]
|
|
144
|
+
);
|
|
145
|
+
return result.rows[0]?.value; // no row ⇒ definitive miss
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async delete(key): Promise<void> {
|
|
149
|
+
await pool.query(
|
|
150
|
+
`DELETE FROM agent_serve_kv WHERE deployment_id = $1 AND key = $2`,
|
|
151
|
+
[deploymentId, key]
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
async list(prefix): Promise<StorageEntry[]> {
|
|
156
|
+
// Escape LIKE wildcards in the prefix so keys containing % or _
|
|
157
|
+
// (URI-encoded segments cannot, but be safe) match literally.
|
|
158
|
+
const escaped = prefix.replace(/([\\%_])/g, "\\$1");
|
|
159
|
+
const result = await pool.query<{ key: string; value: JsonValue }>(
|
|
160
|
+
`SELECT key, value FROM agent_serve_kv
|
|
161
|
+
WHERE deployment_id = $1 AND key LIKE $2 ESCAPE '\\'
|
|
162
|
+
ORDER BY key ASC`,
|
|
163
|
+
[deploymentId, `${escaped}%`]
|
|
164
|
+
);
|
|
165
|
+
return result.rows;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// Playground eval batches → agent_serve_eval_runs (one row per run).
|
|
169
|
+
evals: {
|
|
170
|
+
async put(run, ctx): Promise<void> {
|
|
171
|
+
await pool.query(
|
|
172
|
+
`INSERT INTO agent_serve_eval_runs (deployment_id, agent, run_id, run, updated_at)
|
|
173
|
+
VALUES ($1, $2, $3, $4::jsonb, now())
|
|
174
|
+
ON CONFLICT ON CONSTRAINT pk_agent_serve_eval_runs
|
|
175
|
+
DO UPDATE SET run = EXCLUDED.run, updated_at = now()`,
|
|
176
|
+
[deploymentId, ctx.agentName, run.runId, JSON.stringify(run)]
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
async delete(runId, ctx): Promise<void> {
|
|
180
|
+
await pool.query(
|
|
181
|
+
`DELETE FROM agent_serve_eval_runs
|
|
182
|
+
WHERE deployment_id = $1 AND agent = $2 AND run_id = $3`,
|
|
183
|
+
[deploymentId, ctx.agentName, runId]
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
async list(ctx): Promise<EvalRunSnapshot[]> {
|
|
187
|
+
const result = await pool.query<{ run: EvalRunSnapshot }>(
|
|
188
|
+
`SELECT run FROM agent_serve_eval_runs
|
|
189
|
+
WHERE deployment_id = $1 AND agent = $2`,
|
|
190
|
+
[deploymentId, ctx.agentName]
|
|
191
|
+
);
|
|
192
|
+
return result.rows.map((row) => row.run);
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// A/B metrics → agent_serve_ab: sample rows plus one snapshot row.
|
|
197
|
+
abs: {
|
|
198
|
+
async putSample(sample, ctx): Promise<void> {
|
|
199
|
+
await upsertAbRow(pool, {
|
|
200
|
+
deploymentId,
|
|
201
|
+
agent: ctx.agentName,
|
|
202
|
+
kind: "sample",
|
|
203
|
+
// One boundary event emits a sample per enrolled experiment with
|
|
204
|
+
// the same sessionId/at — the experiment must be in the row key
|
|
205
|
+
// or those samples overwrite each other.
|
|
206
|
+
ref: `${sample.experiment}/${sample.sessionId}/${sample.at}`,
|
|
207
|
+
value: sample,
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
async putSnapshot(snapshot, ctx): Promise<void> {
|
|
211
|
+
await upsertAbRow(pool, {
|
|
212
|
+
deploymentId,
|
|
213
|
+
agent: ctx.agentName,
|
|
214
|
+
kind: "snapshot",
|
|
215
|
+
ref: "",
|
|
216
|
+
value: snapshot,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
async getSnapshot(ctx): Promise<ABSnapshot | undefined> {
|
|
220
|
+
const result = await pool.query<{ value: JsonValue }>(
|
|
221
|
+
`SELECT value FROM agent_serve_ab
|
|
222
|
+
WHERE deployment_id = $1 AND agent = $2 AND kind = 'snapshot' AND ref = ''`,
|
|
223
|
+
[deploymentId, ctx.agentName]
|
|
224
|
+
);
|
|
225
|
+
return result.rows[0]?.value as unknown as ABSnapshot | undefined;
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function upsertAbRow(
|
|
232
|
+
pool: PostgresQueryable,
|
|
233
|
+
row: {
|
|
234
|
+
deploymentId: string;
|
|
235
|
+
agent: string;
|
|
236
|
+
kind: "sample" | "snapshot";
|
|
237
|
+
ref: string;
|
|
238
|
+
value: unknown;
|
|
239
|
+
}
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
await pool.query(
|
|
242
|
+
`INSERT INTO agent_serve_ab (deployment_id, agent, kind, ref, value, updated_at)
|
|
243
|
+
VALUES ($1, $2, $3, $4, $5::jsonb, now())
|
|
244
|
+
ON CONFLICT ON CONSTRAINT pk_agent_serve_ab
|
|
245
|
+
DO UPDATE SET value = EXCLUDED.value, updated_at = now()`,
|
|
246
|
+
[row.deploymentId, row.agent, row.kind, row.ref, JSON.stringify(row.value)]
|
|
247
|
+
);
|
|
248
|
+
}
|
package/src/storage.ts
CHANGED
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
*
|
|
34
34
|
* Because keys are opaque strings to the sink, new kinds of durable state
|
|
35
35
|
* (channel cursors, thread affinity, …) are new key prefixes — existing
|
|
36
|
-
* sinks store them with no code changes.
|
|
36
|
+
* sinks store them with no code changes. Authors can also store their own
|
|
37
|
+
* JSON under `agentkit/v1/{agent}/kv/…` via {@link HostContext.kv}; the
|
|
38
|
+
* framework still mints the full key from the author-supplied segment.
|
|
37
39
|
*
|
|
38
40
|
* Delivery semantics: writes are **serialized** (one sink call in flight
|
|
39
41
|
* per agent, in order), **bounded** (a sink that falls behind sheds writes
|
|
@@ -274,6 +276,7 @@ export const STORAGE_KEY_ROOT = "agentkit/v1" as const;
|
|
|
274
276
|
* | `agentkit/v1/{agent}/session-events/{sessionId}/{index}` | `SessionEvent[]` chunk (index = first event's index, zero-padded) |
|
|
275
277
|
* | `agentkit/v1/{agent}/continuation/{channelId}/{token}` | `{ sessionId }` |
|
|
276
278
|
* | `agentkit/v1/{agent}/reminder/{reminderId}` | `ReminderRecord` |
|
|
279
|
+
* | `agentkit/v1/{agent}/kv/{key}` | Author JSON via {@link HostContext.kv} |
|
|
277
280
|
*
|
|
278
281
|
* Eval-run and A/B history do not flow through this KV scheme — they have
|
|
279
282
|
* dedicated tables ({@link StorageConfig.evals} / {@link StorageConfig.abs}).
|
|
@@ -301,6 +304,13 @@ export const storageKeys = {
|
|
|
301
304
|
`${STORAGE_KEY_ROOT}/${agent}/reminder/${reminderId}`,
|
|
302
305
|
reminderPrefix: (agent: string): string =>
|
|
303
306
|
`${STORAGE_KEY_ROOT}/${agent}/reminder/`,
|
|
307
|
+
/**
|
|
308
|
+
* Author-facing opaque JSON (see {@link HostContext.kv}). `key` is
|
|
309
|
+
* URI-encoded / digested like other caller-controlled segments.
|
|
310
|
+
*/
|
|
311
|
+
kv: (agent: string, key: string): string =>
|
|
312
|
+
`${STORAGE_KEY_ROOT}/${agent}/kv/${keySegment(key)}`,
|
|
313
|
+
kvPrefix: (agent: string): string => `${STORAGE_KEY_ROOT}/${agent}/kv/`,
|
|
304
314
|
} as const;
|
|
305
315
|
|
|
306
316
|
/**
|
package/src/types.ts
CHANGED
|
@@ -316,6 +316,28 @@ export interface HostMcpCallResult {
|
|
|
316
316
|
structuredContent?: JsonObject;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Durable Slack follow-up nudge + ask-claim API attached to {@link HostContext}.
|
|
321
|
+
* Used for mid-turn inject, boundary resume skip, and delayed-parent dedupe.
|
|
322
|
+
*/
|
|
323
|
+
export interface SlackNudgeHostApi {
|
|
324
|
+
append(input: {
|
|
325
|
+
continuationKey: string;
|
|
326
|
+
sourceTs: string;
|
|
327
|
+
text: string;
|
|
328
|
+
author?: string;
|
|
329
|
+
}): Promise<boolean>;
|
|
330
|
+
/** Drain pending nudges and return a `<slack_followup>` block, if any. */
|
|
331
|
+
drainFormatted(continuationKey: string): Promise<string | undefined>;
|
|
332
|
+
clear(continuationKey: string): Promise<void>;
|
|
333
|
+
hasUndelivered(continuationKey: string): Promise<boolean>;
|
|
334
|
+
isDelivered(continuationKey: string, sourceTs: string): Promise<boolean>;
|
|
335
|
+
/** Claim `channelId:message.ts` exactly once (false = already seen). */
|
|
336
|
+
tryClaimAsk(channelId: string, messageTs: string): Promise<boolean>;
|
|
337
|
+
/** Release a claim after a failed send so Slack redelivery can retry. */
|
|
338
|
+
releaseAsk(channelId: string, messageTs: string): Promise<void>;
|
|
339
|
+
}
|
|
340
|
+
|
|
319
341
|
/**
|
|
320
342
|
* Shared host services available on every tool / channel / schedule surface.
|
|
321
343
|
* Channel packs attach this as `ctx.host` so handlers can tap GitHub, Slack,
|
|
@@ -339,6 +361,29 @@ export interface HostContext {
|
|
|
339
361
|
* the serve host has attached a {@link ReminderRunner}.
|
|
340
362
|
*/
|
|
341
363
|
reminders?: ReminderHostApi;
|
|
364
|
+
/**
|
|
365
|
+
* Slack ask-dedupe / optional nudge helpers. Always present on a live
|
|
366
|
+
* {@link SessionEngine} host. Mid-turn nudge inject is not used; busy
|
|
367
|
+
* Slack follow-ups coalesce and drain at turn boundaries.
|
|
368
|
+
*/
|
|
369
|
+
slackNudges?: SlackNudgeHostApi;
|
|
370
|
+
/**
|
|
371
|
+
* Author-facing durable JSON KV (`agentkit/v1/{agent}/kv/…`). Backed by
|
|
372
|
+
* `agent/storage.ts` (`defineStorage`) when authored; otherwise files
|
|
373
|
+
* under `--state-root/kv`. Prefer this over ad-hoc files under the
|
|
374
|
+
* project tree so state survives host replacement.
|
|
375
|
+
*/
|
|
376
|
+
kv: HostKvApi;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Host-facing JSON key-value API attached to {@link HostContext.kv}. */
|
|
380
|
+
export interface HostKvApi {
|
|
381
|
+
/** Read one value. Miss → `undefined`. */
|
|
382
|
+
get(key: string): Promise<JsonValue | undefined>;
|
|
383
|
+
/** Write or replace one value. */
|
|
384
|
+
put(key: string, value: JsonValue): Promise<void>;
|
|
385
|
+
/** Remove one key. No-op when missing. */
|
|
386
|
+
delete(key: string): Promise<void>;
|
|
342
387
|
}
|
|
343
388
|
|
|
344
389
|
/** Runtime context handed to a tool's `execute`. */
|
|
@@ -851,6 +896,11 @@ export interface ChannelSession {
|
|
|
851
896
|
readonly continuationToken: string | null;
|
|
852
897
|
/** True when this `send()` created the session. */
|
|
853
898
|
readonly isNew: boolean;
|
|
899
|
+
/**
|
|
900
|
+
* True when `admission: "coalesce"` enqueued this message behind an
|
|
901
|
+
* in-flight turn instead of starting immediately (Slack busy path).
|
|
902
|
+
*/
|
|
903
|
+
readonly coalesced?: boolean;
|
|
854
904
|
readonly info: SessionInfo;
|
|
855
905
|
/** Replay recorded events from `startIndex`, then follow live (NDJSON). */
|
|
856
906
|
getEventStream(options?: {
|
|
@@ -919,6 +969,17 @@ export interface SendMessageOptions {
|
|
|
919
969
|
* store is gone.
|
|
920
970
|
*/
|
|
921
971
|
sdkAgentId?: string;
|
|
972
|
+
/**
|
|
973
|
+
* Turn admission when a session is already busy.
|
|
974
|
+
* - `"preempt"` (default) — interrupt the in-flight turn (HTTP / MCP).
|
|
975
|
+
* - `"coalesce"` — enqueue and return without interrupting (Slack).
|
|
976
|
+
*/
|
|
977
|
+
admission?: "preempt" | "coalesce";
|
|
978
|
+
/**
|
|
979
|
+
* Optional source message timestamp for coalesce queue items. When a
|
|
980
|
+
* mid-turn nudge already delivered this `sourceTs`, boundary resume skips it.
|
|
981
|
+
*/
|
|
982
|
+
coalesceSourceTs?: string;
|
|
922
983
|
}
|
|
923
984
|
|
|
924
985
|
export type SendMessageFn = (
|
|
@@ -1183,6 +1244,28 @@ export interface ChannelStartArgs {
|
|
|
1183
1244
|
/** Shared host services (MCP / GitHub / Slack). Same as {@link ChannelHandlerArgs.host}. */
|
|
1184
1245
|
host: HostContext;
|
|
1185
1246
|
waitUntil(promise: Promise<unknown>): void;
|
|
1247
|
+
/**
|
|
1248
|
+
* Whether a live session already exists for a continuation token on this
|
|
1249
|
+
* channel (includes lazy restore from storage).
|
|
1250
|
+
*/
|
|
1251
|
+
hasContinuationSession(continuationToken: string): Promise<boolean>;
|
|
1252
|
+
/**
|
|
1253
|
+
* Whether a model turn is in flight (or being admitted) for the session
|
|
1254
|
+
* addressed by this continuation token.
|
|
1255
|
+
*/
|
|
1256
|
+
isContinuationBusy(continuationToken: string): Promise<boolean>;
|
|
1257
|
+
/**
|
|
1258
|
+
* Slack warm-delta watermark: last agent reply ts recorded on the session's
|
|
1259
|
+
* channel state (`lastBotMessageTs`), when present.
|
|
1260
|
+
*/
|
|
1261
|
+
getContinuationLastBotMessageTs(
|
|
1262
|
+
continuationToken: string
|
|
1263
|
+
): Promise<string | undefined>;
|
|
1264
|
+
/**
|
|
1265
|
+
* Interrupt the in-flight turn for a continuation token and clear any
|
|
1266
|
+
* coalesced follow-ups queued behind it. Returns whether work was active.
|
|
1267
|
+
*/
|
|
1268
|
+
interruptContinuation(continuationToken: string): Promise<boolean>;
|
|
1186
1269
|
/**
|
|
1187
1270
|
* Approve or deny a parked {@link ToolConfig.needsApproval} tool call
|
|
1188
1271
|
* for a session on this channel. Channel packs (e.g. Slack Block Kit
|