@agentknight/sdk 0.9.35

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.
@@ -0,0 +1,47 @@
1
+ import {
2
+ SessionAssistantPreviewSchema,
3
+ SessionUserPreviewSchema
4
+ } from "./chunk-ZKAN2FO7.js";
5
+
6
+ // src/assistant-preview.ts
7
+ function isContentfulAssistant(item) {
8
+ return item.kind === "assistant-message" && item.actor === "assistant" && (item.content.trim().length > 0 || (item.attachments ?? []).some((attachment) => attachment.kind === "image"));
9
+ }
10
+ function userPreview(item) {
11
+ if (item.kind !== "human-message" || item.actor !== "human") return void 0;
12
+ return SessionUserPreviewSchema.parse({
13
+ id: item.id,
14
+ revision: item.revision,
15
+ timestamp: item.timestamp,
16
+ text: item.content.slice(0, 4096)
17
+ });
18
+ }
19
+ function latestPreviewItems(items) {
20
+ const latest = [...items].reverse();
21
+ return [
22
+ latest.find((item) => item.kind === "human-message" && item.actor === "human"),
23
+ latest.find(isContentfulAssistant)
24
+ ].filter((item) => item !== void 0);
25
+ }
26
+ function assistantPreview(item, referenceItem) {
27
+ if (item.kind !== "assistant-message" || !isContentfulAssistant(item)) return void 0;
28
+ const referenced = referenceItem(item);
29
+ const attachments = referenced.kind === "assistant-message" ? (referenced.attachments ?? []).filter((attachment) => attachment.kind === "image" && attachment.imageRef === "sha256").slice(0, 4) : [];
30
+ if (item.content.trim().length === 0 && attachments.length === 0) return void 0;
31
+ return SessionAssistantPreviewSchema.parse({
32
+ id: item.id,
33
+ revision: item.revision,
34
+ timestamp: item.timestamp,
35
+ sourceEventIds: item.sourceEventIds.slice(-1),
36
+ text: item.content.trim().slice(0, 4096),
37
+ final: item.final,
38
+ attachments
39
+ });
40
+ }
41
+
42
+ export {
43
+ isContentfulAssistant,
44
+ userPreview,
45
+ latestPreviewItems,
46
+ assistantPreview
47
+ };
@@ -0,0 +1,340 @@
1
+ import {
2
+ SESSION_NAME_MAX_LENGTH,
3
+ SessionCommunicationDataSchema,
4
+ SessionNameSchema,
5
+ WorkbenchOperationStatusSchema
6
+ } from "./chunk-ZKAN2FO7.js";
7
+
8
+ // src/session-operations.ts
9
+ import { createHash, randomUUID } from "node:crypto";
10
+ import { mkdir, open, readFile, rename, unlink } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { z } from "zod";
13
+ var MAX_OPERATIONS = 2048;
14
+ var MAX_ACTIVE_OPERATIONS = 8;
15
+ var MAX_RESULT_BYTES = 8 * 1024 * 1024;
16
+ var RESULT_RETENTION_MS = 15 * 6e4;
17
+ var RECORD_RETENTION_MS = 24 * 60 * 6e4;
18
+ var RecordSchema = z.object({
19
+ key: z.string().length(64),
20
+ fingerprint: z.string().length(64),
21
+ createdAt: z.number().finite().nonnegative(),
22
+ updatedAt: z.number().finite().nonnegative(),
23
+ status: z.object({
24
+ operationId: WorkbenchOperationStatusSchema.shape.operationId,
25
+ operation: WorkbenchOperationStatusSchema.shape.operation,
26
+ agentRunId: WorkbenchOperationStatusSchema.shape.agentRunId,
27
+ error: WorkbenchOperationStatusSchema.shape.error,
28
+ // Completed results are intentionally not written to the operation journal.
29
+ state: z.enum(["running", "succeeded", "failed", "uncertain", "result-expired"])
30
+ }).strict()
31
+ }).strict();
32
+ var JournalSchema = z.object({
33
+ version: z.literal(1),
34
+ records: z.array(RecordSchema).max(MAX_OPERATIONS)
35
+ }).strict();
36
+ var SessionOperationError = class extends Error {
37
+ constructor(code, message, options) {
38
+ super(message, options);
39
+ this.code = code;
40
+ this.name = "SessionOperationError";
41
+ this.outcome = options?.outcome ?? "failed";
42
+ }
43
+ code;
44
+ outcome;
45
+ };
46
+ function stableValue(value) {
47
+ if (Array.isArray(value)) return value.map(stableValue);
48
+ if (value !== null && typeof value === "object") {
49
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, stableValue(item)]));
50
+ }
51
+ return value;
52
+ }
53
+ function digest(value) {
54
+ return createHash("sha256").update(JSON.stringify(stableValue(value))).digest("hex");
55
+ }
56
+ var SessionOperations = class {
57
+ constructor(options) {
58
+ this.options = options;
59
+ this.#path = join(options.directory, "workbench-operations.json");
60
+ }
61
+ options;
62
+ #records = /* @__PURE__ */ new Map();
63
+ #active = /* @__PURE__ */ new Set();
64
+ #path;
65
+ #initialization;
66
+ #transition = Promise.resolve();
67
+ #fatalError;
68
+ #closed = false;
69
+ #expiryTimer;
70
+ #now() {
71
+ return this.options.now?.() ?? Date.now();
72
+ }
73
+ get hasActiveOperations() {
74
+ return this.#active.size > 0;
75
+ }
76
+ #key(owner, operationId) {
77
+ return digest([this.options.namespace ?? "", owner.clientId, owner.sessionId, operationId]);
78
+ }
79
+ async submit(options) {
80
+ await this.#initialize();
81
+ return this.#serialize(async () => {
82
+ if (this.#closed) throw new SessionOperationError("OPERATIONS_CLOSED", "The PC is stopping");
83
+ if (options.expectedAgentRunId !== this.options.agentRunId) {
84
+ throw new SessionOperationError("RUNTIME_CHANGED", "The PC restarted; refresh before starting an operation");
85
+ }
86
+ const createdAt = Date.parse(options.operationCreatedAt);
87
+ if (!Number.isFinite(createdAt) || createdAt > this.#now() + 5 * 6e4) {
88
+ throw new SessionOperationError("CLOCK_MISMATCH", "Check the phone and PC clocks before starting an operation");
89
+ }
90
+ if (this.#now() - createdAt >= RECORD_RETENTION_MS) {
91
+ throw new SessionOperationError("OPERATION_EXPIRED", "The submission expired; it has not been retried");
92
+ }
93
+ const key = this.#key(options, options.operationId);
94
+ const fingerprint = digest([options.operation, options.operationCreatedAt, options.data]);
95
+ const existing = this.#records.get(key);
96
+ if (existing !== void 0) {
97
+ if (existing.fingerprint !== fingerprint) {
98
+ throw new SessionOperationError("OPERATION_CONFLICT", "The operation ID was reused with different data");
99
+ }
100
+ return this.#publicStatus(existing);
101
+ }
102
+ if (this.#active.size >= MAX_ACTIVE_OPERATIONS) {
103
+ throw new SessionOperationError("OPERATION_BUSY", "Too many session operations are still running");
104
+ }
105
+ this.#prune();
106
+ if (this.#records.size >= MAX_OPERATIONS) {
107
+ throw new SessionOperationError("OPERATION_CAPACITY", "The operation journal is full");
108
+ }
109
+ const record = {
110
+ key,
111
+ fingerprint,
112
+ createdAt,
113
+ updatedAt: this.#now(),
114
+ status: {
115
+ operationId: options.operationId,
116
+ operation: options.operation,
117
+ agentRunId: this.options.agentRunId,
118
+ state: "running"
119
+ }
120
+ };
121
+ this.#records.set(key, record);
122
+ await this.#persist();
123
+ const work = Promise.resolve().then(options.execute).then((result) => SessionCommunicationDataSchema.parse(result)).then(
124
+ (result) => this.#settle(record, { state: "succeeded", result }),
125
+ (error) => {
126
+ this.options.onError(error);
127
+ return this.#settle(record, {
128
+ state: error instanceof SessionOperationError ? error.outcome : "uncertain",
129
+ error: {
130
+ code: error instanceof SessionOperationError ? error.code : "OPERATION_FAILED",
131
+ message: error instanceof SessionOperationError ? error.message : "The result could not be confirmed; the operation was not retried"
132
+ }
133
+ });
134
+ }
135
+ ).catch((error) => {
136
+ this.options.onError(error);
137
+ }).finally(() => this.#active.delete(work));
138
+ this.#active.add(work);
139
+ return this.#publicStatus(record);
140
+ });
141
+ }
142
+ async get(owner, operationId) {
143
+ await this.#initialize();
144
+ return this.#serialize(async () => {
145
+ const record = this.#records.get(this.#key(owner, operationId));
146
+ if (record === void 0) {
147
+ throw new SessionOperationError("OPERATION_NOT_FOUND", "The operation is unavailable; it has not been retried");
148
+ }
149
+ return this.#publicStatus(record);
150
+ });
151
+ }
152
+ async stopAccepting() {
153
+ this.#closed = true;
154
+ this.#scheduleExpiration();
155
+ await this.#initialization;
156
+ await this.#transition;
157
+ }
158
+ async close() {
159
+ await this.stopAccepting();
160
+ await Promise.all(this.#active);
161
+ await this.#transition;
162
+ }
163
+ #publicStatus(record) {
164
+ this.#expire(record);
165
+ return WorkbenchOperationStatusSchema.parse(record.status);
166
+ }
167
+ #expire(record, force = false) {
168
+ if (record.status.state === "succeeded" && (force || this.#closed || this.#now() - record.updatedAt >= RESULT_RETENTION_MS)) {
169
+ const { result: _result, ...status } = record.status;
170
+ record.status = { ...status, state: "result-expired" };
171
+ }
172
+ }
173
+ #scheduleExpiration() {
174
+ clearTimeout(this.#expiryTimer);
175
+ this.#expiryTimer = void 0;
176
+ let next = Infinity;
177
+ let bytes = 0;
178
+ const results = [];
179
+ for (const record of this.#records.values()) {
180
+ this.#expire(record);
181
+ if (record.status.state === "succeeded") {
182
+ const size = Buffer.byteLength(JSON.stringify(record.status.result));
183
+ bytes += size;
184
+ results.push({ record, size });
185
+ }
186
+ }
187
+ results.sort((left, right) => left.record.updatedAt - right.record.updatedAt);
188
+ for (const { record, size } of results) {
189
+ if (bytes > MAX_RESULT_BYTES) {
190
+ this.#expire(record, true);
191
+ bytes -= size;
192
+ } else {
193
+ next = Math.min(next, record.updatedAt + RESULT_RETENTION_MS);
194
+ }
195
+ }
196
+ if (Number.isFinite(next)) {
197
+ this.#expiryTimer = setTimeout(() => this.#scheduleExpiration(), Math.max(1, next - this.#now()));
198
+ this.#expiryTimer.unref();
199
+ }
200
+ }
201
+ async #settle(record, outcome) {
202
+ await this.#serialize(async () => {
203
+ const { result: _result, error: _error, ...identity } = record.status;
204
+ record.status = { ...identity, ...outcome };
205
+ record.updatedAt = this.#now();
206
+ this.#scheduleExpiration();
207
+ await this.#persist();
208
+ });
209
+ }
210
+ #prune() {
211
+ for (const [key, record] of this.#records) {
212
+ if (record.status.state !== "running" && this.#now() - record.createdAt >= RECORD_RETENTION_MS) {
213
+ this.#records.delete(key);
214
+ }
215
+ }
216
+ }
217
+ #initialize() {
218
+ this.#initialization ??= this.#load();
219
+ return this.#initialization;
220
+ }
221
+ async #load() {
222
+ await mkdir(this.options.directory, { recursive: true });
223
+ let content;
224
+ try {
225
+ content = await readFile(this.#path, "utf8");
226
+ } catch (error) {
227
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
228
+ throw error;
229
+ }
230
+ if (Buffer.byteLength(content) > 4 * 1024 * 1024) throw new Error("Operation journal is too large");
231
+ const journal = JournalSchema.parse(JSON.parse(content));
232
+ for (const stored of journal.records) {
233
+ if (this.#records.has(stored.key)) throw new Error("Duplicate operation journal entry");
234
+ const status = stored.status.state === "running" ? {
235
+ ...stored.status,
236
+ state: "uncertain",
237
+ error: { code: "OPERATION_INTERRUPTED", message: "The PC restarted before the result was recorded; the operation was not retried" }
238
+ } : stored.status.state === "succeeded" ? { ...stored.status, state: "result-expired" } : stored.status;
239
+ this.#records.set(stored.key, { ...stored, status });
240
+ }
241
+ }
242
+ async #persist() {
243
+ const temporary = `${this.#path}.${randomUUID()}.tmp`;
244
+ try {
245
+ const records = [...this.#records.values()].map((record) => {
246
+ const { result: _result, ...status } = record.status;
247
+ return { ...record, status };
248
+ });
249
+ const handle = await open(temporary, "wx", 384);
250
+ try {
251
+ await handle.writeFile(`${JSON.stringify({ version: 1, records })}
252
+ `);
253
+ await handle.sync();
254
+ } finally {
255
+ await handle.close();
256
+ }
257
+ await rename(temporary, this.#path);
258
+ } catch (error) {
259
+ this.#fatalError = new Error("The operation journal could not be saved", { cause: error });
260
+ try {
261
+ await unlink(temporary);
262
+ } catch (cleanupError) {
263
+ if (!(cleanupError instanceof Error && "code" in cleanupError && cleanupError.code === "ENOENT")) {
264
+ this.options.onError(cleanupError);
265
+ }
266
+ }
267
+ throw this.#fatalError;
268
+ }
269
+ }
270
+ #serialize(run) {
271
+ const next = this.#transition.then(() => {
272
+ if (this.#fatalError !== void 0) throw this.#fatalError;
273
+ return run();
274
+ });
275
+ this.#transition = next.then(() => void 0, () => void 0);
276
+ return next;
277
+ }
278
+ };
279
+
280
+ // src/coding-agent-adapter.ts
281
+ var AgentModelListingError = class extends AggregateError {
282
+ constructor(errors, availableModels) {
283
+ super(errors, errors.map((error) => error.message).join("; "));
284
+ this.availableModels = availableModels;
285
+ this.name = "AgentModelListingError";
286
+ }
287
+ availableModels;
288
+ };
289
+ function captureResearchAccess(adapter, sessionId) {
290
+ if (adapter.agentId === "managed" && adapter.captureResearchAccess !== void 0) {
291
+ return adapter.captureResearchAccess(sessionId);
292
+ }
293
+ return { detached: false, assertCurrent: captureWorkbenchAccess(adapter, sessionId) };
294
+ }
295
+ function captureWorkbenchAccess(adapter, sessionId) {
296
+ if (adapter.workbench === void 0 || adapter.agentId !== "copilot" && adapter.agentId !== "managed" || adapter.agentId === "managed" && adapter.captureWorkbenchAccess === void 0 || adapter.agentId === "copilot" && sessionId.startsWith("managed:")) {
297
+ throw new SessionOperationError("AGENT_OPERATION_UNSUPPORTED", "This coding agent does not support the session workbench");
298
+ }
299
+ return adapter.captureWorkbenchAccess?.(sessionId) ?? (() => {
300
+ if (adapter.isSessionVisible?.(sessionId) === false) {
301
+ throw new SessionOperationError("SESSION_NOT_SHARED", "This session is no longer shared");
302
+ }
303
+ });
304
+ }
305
+ var UnsupportedAgentOperationError = class extends Error {
306
+ code = "AGENT_OPERATION_UNSUPPORTED";
307
+ constructor(agentId, operation) {
308
+ super(`${agentId} does not support ${operation}`);
309
+ this.name = "UnsupportedAgentOperationError";
310
+ }
311
+ };
312
+ var AgentSessionRenameError = class extends Error {
313
+ constructor(code, message) {
314
+ super(message);
315
+ this.code = code;
316
+ this.name = "AgentSessionRenameError";
317
+ }
318
+ code;
319
+ };
320
+ function validateSessionName(name) {
321
+ const result = SessionNameSchema.safeParse(name);
322
+ if (!result.success) {
323
+ throw new AgentSessionRenameError(
324
+ "INVALID_SESSION_NAME",
325
+ `Enter a nonempty single-line session name of at most ${SESSION_NAME_MAX_LENGTH} characters.`
326
+ );
327
+ }
328
+ return result.data;
329
+ }
330
+
331
+ export {
332
+ SessionOperationError,
333
+ SessionOperations,
334
+ AgentModelListingError,
335
+ captureResearchAccess,
336
+ captureWorkbenchAccess,
337
+ UnsupportedAgentOperationError,
338
+ AgentSessionRenameError,
339
+ validateSessionName
340
+ };
@@ -0,0 +1,139 @@
1
+ import {
2
+ IdSchema
3
+ } from "./chunk-ZKAN2FO7.js";
4
+
5
+ // src/data-directory.ts
6
+ import { randomUUID } from "node:crypto";
7
+ import { mkdir, open, readFile, realpath, rm } from "node:fs/promises";
8
+ import { isAbsolute, join, resolve } from "node:path";
9
+ import { z } from "zod";
10
+ var OwnerSchema = z.object({ pid: z.number().int().positive(), instanceId: z.uuid().optional() });
11
+ function validateDataDirectory(directory) {
12
+ if (typeof directory !== "string" || directory.trim().length === 0 || !isAbsolute(directory)) {
13
+ throw new Error("dataDirectory must be an explicitly configured absolute path.");
14
+ }
15
+ return resolve(directory);
16
+ }
17
+ async function readOwner(path) {
18
+ try {
19
+ return OwnerSchema.parse(JSON.parse(await readFile(path, "utf8")));
20
+ } catch (error) {
21
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
22
+ throw error;
23
+ }
24
+ }
25
+ function processExists(pid) {
26
+ try {
27
+ process.kill(pid, 0);
28
+ return true;
29
+ } catch (error) {
30
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") return false;
31
+ throw error;
32
+ }
33
+ }
34
+ var DataDirectoryLease = class _DataDirectoryLease {
35
+ constructor(directory, instanceId, lockPath) {
36
+ this.directory = directory;
37
+ this.instanceId = instanceId;
38
+ this.lockPath = lockPath;
39
+ }
40
+ directory;
41
+ instanceId;
42
+ lockPath;
43
+ #released = false;
44
+ #attached = false;
45
+ static async acquire(dataDirectory) {
46
+ const requested = validateDataDirectory(dataDirectory);
47
+ await mkdir(requested, { recursive: true, mode: 448 });
48
+ const directory = await realpath(requested);
49
+ const runDirectory = join(directory, "run");
50
+ await mkdir(runDirectory, { recursive: true, mode: 448 });
51
+ const lockPath = join(runDirectory, "instance.lock");
52
+ let lock;
53
+ try {
54
+ lock = await open(lockPath, "wx", 384);
55
+ } catch (error) {
56
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
57
+ const recovery = join(runDirectory, "lock-recovery");
58
+ try {
59
+ await mkdir(recovery, { mode: 448 });
60
+ } catch (cause) {
61
+ throw new Error(`Another startup is recovering ${lockPath}. Retry; if interrupted, inspect ${recovery}.`, { cause });
62
+ }
63
+ try {
64
+ const previous = await readOwner(lockPath);
65
+ if (previous !== void 0) {
66
+ if (processExists(previous.pid)) throw new Error(`An Agent is already running (PID ${previous.pid}).`);
67
+ await rm(lockPath);
68
+ }
69
+ lock = await open(lockPath, "wx", 384);
70
+ } finally {
71
+ await rm(recovery, { recursive: true });
72
+ }
73
+ }
74
+ const instanceId = randomUUID();
75
+ try {
76
+ await lock.writeFile(JSON.stringify({ pid: process.pid, instanceId }));
77
+ } catch (error) {
78
+ await lock.close();
79
+ await rm(lockPath);
80
+ throw error;
81
+ }
82
+ await lock.close();
83
+ return new _DataDirectoryLease(directory, instanceId, lockPath);
84
+ }
85
+ async attach(dataDirectory) {
86
+ if (this.#released || this.#attached) throw new Error("Data directory lease is released or already attached to an SDK instance.");
87
+ if (await realpath(validateDataDirectory(dataDirectory)) !== this.directory) {
88
+ throw new Error("Data directory lease belongs to another directory.");
89
+ }
90
+ if (this.#released || this.#attached) throw new Error("Data directory lease is released or already attached to an SDK instance.");
91
+ this.#attached = true;
92
+ return () => {
93
+ this.#attached = false;
94
+ };
95
+ }
96
+ async release() {
97
+ if (this.#released) return;
98
+ if (this.#attached) throw new Error("Stop the SDK instance before releasing its data directory.");
99
+ const owner = await readOwner(this.lockPath);
100
+ if (owner?.pid !== process.pid || owner.instanceId !== this.instanceId) {
101
+ throw new Error("Data directory ownership changed; refusing to remove another instance's lock.");
102
+ }
103
+ await rm(this.lockPath);
104
+ this.#released = true;
105
+ }
106
+ };
107
+
108
+ // src/peer-chat-request.ts
109
+ import { z as z2 } from "zod";
110
+ var PEER_CHAT_TIMEOUT_MS = 12e4;
111
+ var PeerChatRequestSchema = z2.object({
112
+ sessionId: IdSchema,
113
+ targetMachineId: IdSchema,
114
+ targetSessionId: IdSchema,
115
+ prompt: z2.string().trim().min(1).max(16e3)
116
+ });
117
+ var PeerConversationSchema = z2.object({
118
+ contextId: IdSchema,
119
+ exchanges: z2.array(z2.object({
120
+ taskId: IdSchema,
121
+ requestMessageId: IdSchema,
122
+ responseMessageId: IdSchema,
123
+ request: z2.string(),
124
+ response: z2.string()
125
+ }))
126
+ });
127
+ var PeerChatResultSchema = z2.object({
128
+ reply: z2.string(),
129
+ peerReplies: z2.array(z2.string()),
130
+ conversation: PeerConversationSchema.optional()
131
+ });
132
+
133
+ export {
134
+ validateDataDirectory,
135
+ DataDirectoryLease,
136
+ PEER_CHAT_TIMEOUT_MS,
137
+ PeerChatRequestSchema,
138
+ PeerChatResultSchema
139
+ };