@abelxiaoxing/cadence 2.0.12 → 2.0.14
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 +141 -54
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/docs/remote-access.md +13 -0
- package/docs/reuse-and-distribution.md +13 -2
- package/docs/service-management.md +31 -12
- package/licenses/pi-web-ui-reuse.md +2 -0
- package/package.json +5 -2
- package/src/conversation-context.mjs +147 -0
- package/src/conversation-resources.mjs +47 -0
- package/src/native-build.ts +1 -1
- package/src/native-client.ts +2 -2
- package/src/operator-cli.mjs +4 -7
- package/src/sdk-worker.mjs +41 -8
- package/src/service/contracts/workbench.ts +303 -0
- package/src/service/coordination.ts +1 -0
- package/src/service-cli.mjs +1227 -577
- package/src/service-client.ts +49 -0
- package/src/service-host.mjs +15768 -13011
- package/src/service-http-client.ts +50 -24
- package/src/service-wire.ts +26 -4
- package/src/task-store.ts +12 -11
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +13 -7
- package/web/dist/manifest.json +7 -7
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Private ordinary-chat context. No Pi session import, hidden thinking or provider envelopes. */
|
|
2
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
canonicalJson,
|
|
5
|
+
closed,
|
|
6
|
+
integer,
|
|
7
|
+
list,
|
|
8
|
+
oneOf,
|
|
9
|
+
parseWireJson,
|
|
10
|
+
text,
|
|
11
|
+
} from "./worker-public.mjs";
|
|
12
|
+
import { DetailSanitizer, sanitizeText } from "./worker-redaction.mjs";
|
|
13
|
+
|
|
14
|
+
export const CONTEXT_BYTES = 2 * 1024 * 1024;
|
|
15
|
+
const clean = (s, secrets, max = CONTEXT_BYTES) =>
|
|
16
|
+
sanitizeText(text(s, max), secrets);
|
|
17
|
+
function argumentsData(value, secrets) {
|
|
18
|
+
const sanitizer = new DetailSanitizer("canonical-json", secrets);
|
|
19
|
+
const source =
|
|
20
|
+
sanitizer.write(canonicalJson(value, 64 * 1024)) + sanitizer.end();
|
|
21
|
+
if (sanitizer.limited) throw new Error("Unsupported context arguments");
|
|
22
|
+
return parseWireJson(source, 64 * 1024);
|
|
23
|
+
}
|
|
24
|
+
/** Deliberately retain text and tool exchanges, not just last assistant prose. */
|
|
25
|
+
export function conversationMessages(messages, secrets = []) {
|
|
26
|
+
return list(messages, 4096, (message) => {
|
|
27
|
+
if (!message || typeof message !== "object")
|
|
28
|
+
throw new Error("Invalid context");
|
|
29
|
+
const role = oneOf(message.role, ["user", "assistant", "toolResult"]);
|
|
30
|
+
const timestamp = integer(message.timestamp, 0);
|
|
31
|
+
const content =
|
|
32
|
+
typeof message.content === "string"
|
|
33
|
+
? [{ type: "text", text: clean(message.content, secrets) }]
|
|
34
|
+
: list(message.content, 256, (block) => {
|
|
35
|
+
if (block.type === "text")
|
|
36
|
+
return { type: "text", text: clean(block.text, secrets) };
|
|
37
|
+
if (role === "assistant" && block.type === "toolCall")
|
|
38
|
+
return {
|
|
39
|
+
type: "toolCall",
|
|
40
|
+
id: clean(block.id, secrets, 512),
|
|
41
|
+
name: clean(block.name, secrets, 128),
|
|
42
|
+
arguments: argumentsData(block.arguments, secrets),
|
|
43
|
+
};
|
|
44
|
+
// Images, hidden thinking and opaque provider signatures never become retained context.
|
|
45
|
+
return null;
|
|
46
|
+
}).filter(Boolean);
|
|
47
|
+
if (role === "user") return { role, timestamp, content };
|
|
48
|
+
if (role === "toolResult")
|
|
49
|
+
return {
|
|
50
|
+
role,
|
|
51
|
+
timestamp,
|
|
52
|
+
content,
|
|
53
|
+
toolCallId: clean(message.toolCallId, secrets, 512),
|
|
54
|
+
toolName: clean(message.toolName, secrets, 128),
|
|
55
|
+
isError: message.isError === true,
|
|
56
|
+
};
|
|
57
|
+
const usage = closed(message.usage, [
|
|
58
|
+
"input",
|
|
59
|
+
"output",
|
|
60
|
+
"cacheRead",
|
|
61
|
+
"cacheWrite",
|
|
62
|
+
"totalTokens",
|
|
63
|
+
"cost",
|
|
64
|
+
]);
|
|
65
|
+
const cost = closed(usage.cost, [
|
|
66
|
+
"input",
|
|
67
|
+
"output",
|
|
68
|
+
"cacheRead",
|
|
69
|
+
"cacheWrite",
|
|
70
|
+
"total",
|
|
71
|
+
]);
|
|
72
|
+
for (const value of [
|
|
73
|
+
...Object.values(usage).filter((v) => typeof v !== "object"),
|
|
74
|
+
...Object.values(cost),
|
|
75
|
+
])
|
|
76
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
77
|
+
throw new Error("Invalid usage");
|
|
78
|
+
return {
|
|
79
|
+
role,
|
|
80
|
+
timestamp,
|
|
81
|
+
content,
|
|
82
|
+
api: clean(message.api, secrets, 128),
|
|
83
|
+
provider: clean(message.provider, secrets, 128),
|
|
84
|
+
model: clean(message.model, secrets, 256),
|
|
85
|
+
usage,
|
|
86
|
+
stopReason: oneOf(message.stopReason, [
|
|
87
|
+
"stop",
|
|
88
|
+
"length",
|
|
89
|
+
"toolUse",
|
|
90
|
+
"error",
|
|
91
|
+
"aborted",
|
|
92
|
+
]),
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function readConversationContext(path) {
|
|
97
|
+
if (path === null) return [];
|
|
98
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
99
|
+
try {
|
|
100
|
+
const st = fstatSync(fd);
|
|
101
|
+
if (
|
|
102
|
+
!st.isFile() ||
|
|
103
|
+
st.nlink !== 1 ||
|
|
104
|
+
st.size > CONTEXT_BYTES ||
|
|
105
|
+
(process.platform !== "win32" &&
|
|
106
|
+
(st.mode & 0o077 || st.uid !== process.getuid()))
|
|
107
|
+
)
|
|
108
|
+
throw new Error("Invalid context file");
|
|
109
|
+
const buffer = Buffer.alloc(st.size + 1);
|
|
110
|
+
let size = 0;
|
|
111
|
+
while (size < buffer.length) {
|
|
112
|
+
const n = readSync(fd, buffer, size, buffer.length - size, null);
|
|
113
|
+
if (!n) break;
|
|
114
|
+
size += n;
|
|
115
|
+
}
|
|
116
|
+
const after = fstatSync(fd);
|
|
117
|
+
if (
|
|
118
|
+
size !== st.size ||
|
|
119
|
+
after.size !== st.size ||
|
|
120
|
+
after.mtimeMs !== st.mtimeMs
|
|
121
|
+
)
|
|
122
|
+
throw new Error("Changed context file");
|
|
123
|
+
const o = closed(
|
|
124
|
+
parseWireJson(
|
|
125
|
+
new TextDecoder("utf-8", { fatal: true }).decode(
|
|
126
|
+
buffer.subarray(0, size),
|
|
127
|
+
),
|
|
128
|
+
CONTEXT_BYTES,
|
|
129
|
+
),
|
|
130
|
+
["schema", "messages"],
|
|
131
|
+
);
|
|
132
|
+
if (o.schema !== "cadence-conversation-context/1")
|
|
133
|
+
throw new Error("Invalid context format");
|
|
134
|
+
return conversationMessages(o.messages);
|
|
135
|
+
} finally {
|
|
136
|
+
closeSync(fd);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function encodeConversationContext(messages, secrets) {
|
|
140
|
+
return canonicalJson(
|
|
141
|
+
{
|
|
142
|
+
schema: "cadence-conversation-context/1",
|
|
143
|
+
messages: conversationMessages(messages, secrets),
|
|
144
|
+
},
|
|
145
|
+
CONTEXT_BYTES,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Ordinary Pi context only: no extension/plugin executor, persona or prompt composer. */
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
loadProjectContextFiles,
|
|
6
|
+
loadSkills,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
function optional(path) {
|
|
10
|
+
try {
|
|
11
|
+
if (!statSync(path).isFile() || statSync(path).size > 64 * 1024)
|
|
12
|
+
throw new Error("Resource too large");
|
|
13
|
+
return readFileSync(path, "utf8");
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error?.code === "ENOENT") return undefined;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function conversationResources(base, cwd, agentDir) {
|
|
20
|
+
const project = join(cwd, ".pi");
|
|
21
|
+
const system =
|
|
22
|
+
optional(join(project, "SYSTEM.md")) ??
|
|
23
|
+
optional(join(agentDir, "SYSTEM.md"));
|
|
24
|
+
const append = [
|
|
25
|
+
optional(join(agentDir, "APPEND_SYSTEM.md")),
|
|
26
|
+
optional(join(project, "APPEND_SYSTEM.md")),
|
|
27
|
+
].filter((v) => v !== undefined);
|
|
28
|
+
const agentsFiles = loadProjectContextFiles({ cwd, agentDir });
|
|
29
|
+
const skills = loadSkills({
|
|
30
|
+
cwd,
|
|
31
|
+
agentDir,
|
|
32
|
+
skillPaths: [],
|
|
33
|
+
includeDefaults: true,
|
|
34
|
+
});
|
|
35
|
+
if (
|
|
36
|
+
Buffer.byteLength(JSON.stringify({ system, append, agentsFiles, skills })) >
|
|
37
|
+
256 * 1024
|
|
38
|
+
)
|
|
39
|
+
throw new Error("Resources too large");
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
getSystemPrompt: () => system,
|
|
43
|
+
getAppendSystemPrompt: () => append,
|
|
44
|
+
getAgentsFiles: () => ({ agentsFiles }),
|
|
45
|
+
getSkills: () => skills,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/native-build.ts
CHANGED
package/src/native-client.ts
CHANGED
|
@@ -192,8 +192,8 @@ export class NativeConnection {
|
|
|
192
192
|
// Failed detach keeps its exact transport/slot. Explicit re-entry may retry
|
|
193
193
|
// detach, but never forget it and allocate a replacement on uncertainty.
|
|
194
194
|
await this.detach();
|
|
195
|
-
//
|
|
196
|
-
//
|
|
195
|
+
// Native clients require an explicit project; never create or redirect to
|
|
196
|
+
// a default workspace. Observation only.
|
|
197
197
|
const access = new ServiceCoordination(
|
|
198
198
|
effectiveWorkspace(cwd).workspace,
|
|
199
199
|
).localAccess();
|
package/src/operator-cli.mjs
CHANGED
|
@@ -288,13 +288,10 @@ function stateHomeInside(root, stateHome = defaultStateHome()) {
|
|
|
288
288
|
function privateWorkspace(requested, stateHome = defaultStateHome()) {
|
|
289
289
|
return contains(join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1"), canonicalAmbientRoot(requested));
|
|
290
290
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
296
|
-
return { workspace: requested, redirected: false };
|
|
297
|
-
return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
|
|
291
|
+
function effectiveWorkspace(requested, stateHome, _home) {
|
|
292
|
+
if (privateWorkspace(requested, stateHome) || stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
293
|
+
throw new Error("Choose a project outside Cadence private state; no default workspace was created");
|
|
294
|
+
return { workspace: requested, redirected: false };
|
|
298
295
|
}
|
|
299
296
|
|
|
300
297
|
class TaskStore {
|
package/src/sdk-worker.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Isolated SDK child, not Pi CLI startup. Task discovery stays disabled;
|
|
2
|
+
// ordinary conversations explicitly load only context/skills, never extensions.
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
2
4
|
import {
|
|
3
5
|
createProvider,
|
|
4
6
|
InMemoryCredentialStore,
|
|
@@ -12,6 +14,11 @@ import {
|
|
|
12
14
|
SessionManager,
|
|
13
15
|
SettingsManager,
|
|
14
16
|
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import {
|
|
18
|
+
encodeConversationContext,
|
|
19
|
+
readConversationContext,
|
|
20
|
+
} from "./conversation-context.mjs";
|
|
21
|
+
import { conversationResources } from "./conversation-resources.mjs";
|
|
15
22
|
import { CaptureEmitter, SdkCapture } from "./worker-capture.mjs";
|
|
16
23
|
import {
|
|
17
24
|
closed,
|
|
@@ -185,13 +192,27 @@ try {
|
|
|
185
192
|
if (inputBytes > 128 * 1024) refuse("parse", "input-bound");
|
|
186
193
|
chunks.push(chunk);
|
|
187
194
|
}
|
|
195
|
+
const parsed = parseWireJson(
|
|
196
|
+
new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)),
|
|
197
|
+
LIMITS.workerRequestBytes,
|
|
198
|
+
);
|
|
188
199
|
const envelope = closed(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
["request", "cwd", "model", "auth"],
|
|
200
|
+
parsed,
|
|
201
|
+
Object.hasOwn(parsed ?? {}, "conversation")
|
|
202
|
+
? ["request", "cwd", "model", "auth", "conversation"]
|
|
203
|
+
: ["request", "cwd", "model", "auth"],
|
|
194
204
|
);
|
|
205
|
+
const conversation =
|
|
206
|
+
envelope.conversation === undefined
|
|
207
|
+
? null
|
|
208
|
+
: closed(envelope.conversation, ["previous", "agentDir"]);
|
|
209
|
+
if (
|
|
210
|
+
conversation &&
|
|
211
|
+
((conversation.previous !== null &&
|
|
212
|
+
typeof conversation.previous !== "string") ||
|
|
213
|
+
typeof conversation.agentDir !== "string")
|
|
214
|
+
)
|
|
215
|
+
refuse("parse", "invalid-conversation");
|
|
195
216
|
const publicRequest = decodeWorkerPublicRequest(envelope.request);
|
|
196
217
|
const request = {
|
|
197
218
|
...envelope,
|
|
@@ -329,6 +350,10 @@ try {
|
|
|
329
350
|
process.stdout.on("error", stdoutFailureListener);
|
|
330
351
|
stage = "session";
|
|
331
352
|
const requestedThinkingLevel = thinkingLevel ?? "off";
|
|
353
|
+
const manager = SessionManager.inMemory(cwd);
|
|
354
|
+
if (conversation)
|
|
355
|
+
for (const message of readConversationContext(conversation.previous))
|
|
356
|
+
manager.appendMessage(message);
|
|
332
357
|
const created = await createAgentSession({
|
|
333
358
|
cwd,
|
|
334
359
|
agentDir: cwd,
|
|
@@ -351,8 +376,10 @@ try {
|
|
|
351
376
|
),
|
|
352
377
|
]
|
|
353
378
|
: [],
|
|
354
|
-
resourceLoader
|
|
355
|
-
|
|
379
|
+
resourceLoader: conversation
|
|
380
|
+
? conversationResources(resourceLoader, cwd, conversation.agentDir)
|
|
381
|
+
: resourceLoader,
|
|
382
|
+
sessionManager: manager,
|
|
356
383
|
settingsManager: SettingsManager.inMemory({
|
|
357
384
|
compaction: { enabled: false },
|
|
358
385
|
retry: { enabled: false },
|
|
@@ -483,6 +510,12 @@ try {
|
|
|
483
510
|
stage = "assistant-final";
|
|
484
511
|
if (failure || terminalStopReason !== "stop" || ingress.frames !== 0)
|
|
485
512
|
refuse("assistant-final", "terminal-incomplete");
|
|
513
|
+
// Anonymous pipe only. The owner applies its access-secret sanitizer before
|
|
514
|
+
// any checkpoint reaches disk; worker output cannot contain owner credentials.
|
|
515
|
+
if (conversation) {
|
|
516
|
+
stage = "conversation-context";
|
|
517
|
+
writeFileSync(3, encodeConversationContext(session.messages, secrets));
|
|
518
|
+
}
|
|
486
519
|
await capture.final();
|
|
487
520
|
await emitter.drain();
|
|
488
521
|
} catch {
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/** Finite, authenticated workbench API. Ordinary chat is not task admission. */
|
|
2
|
+
import {
|
|
3
|
+
bool,
|
|
4
|
+
canonicalJson,
|
|
5
|
+
closed,
|
|
6
|
+
decodeId,
|
|
7
|
+
integer,
|
|
8
|
+
list,
|
|
9
|
+
nullable,
|
|
10
|
+
oneOf,
|
|
11
|
+
text,
|
|
12
|
+
} from "./core.ts";
|
|
13
|
+
import { type ChoiceVersion, decodeChoiceVersion } from "./models.ts";
|
|
14
|
+
|
|
15
|
+
export type WorkbenchRead =
|
|
16
|
+
| { kind: "conversations" }
|
|
17
|
+
| { kind: "task-origin"; taskId: string }
|
|
18
|
+
| { kind: "conversation"; id: string; before: string | null }
|
|
19
|
+
| { kind: "files"; path: string }
|
|
20
|
+
| { kind: "file"; path: string }
|
|
21
|
+
| { kind: "directories"; path: string }
|
|
22
|
+
| { kind: "projects" };
|
|
23
|
+
export type WorkbenchCommand =
|
|
24
|
+
| {
|
|
25
|
+
kind: "link-task";
|
|
26
|
+
commandId: string;
|
|
27
|
+
epoch: string;
|
|
28
|
+
taskId: string;
|
|
29
|
+
conversationId: string;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
kind: "send";
|
|
33
|
+
commandId: string;
|
|
34
|
+
conversationId: string;
|
|
35
|
+
head: string | null;
|
|
36
|
+
text: string;
|
|
37
|
+
model: ChoiceVersion;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
kind: "interrupt";
|
|
41
|
+
commandId: string;
|
|
42
|
+
conversationId: string;
|
|
43
|
+
turnId: string;
|
|
44
|
+
epoch: string;
|
|
45
|
+
}
|
|
46
|
+
| { kind: "open-project"; commandId: string; path: string; epoch: string };
|
|
47
|
+
export type ChatStatus =
|
|
48
|
+
| "queued"
|
|
49
|
+
| "running"
|
|
50
|
+
| "completed"
|
|
51
|
+
| "failed"
|
|
52
|
+
| "canceled"
|
|
53
|
+
| "timeout"
|
|
54
|
+
| "unknown";
|
|
55
|
+
export interface ChatItem {
|
|
56
|
+
id: string;
|
|
57
|
+
role: "user" | "assistant" | "tool";
|
|
58
|
+
text: string;
|
|
59
|
+
name: string;
|
|
60
|
+
args: string;
|
|
61
|
+
error: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface ChatTurn {
|
|
64
|
+
id: string;
|
|
65
|
+
epoch: string;
|
|
66
|
+
status: ChatStatus;
|
|
67
|
+
items: ChatItem[];
|
|
68
|
+
notice: string;
|
|
69
|
+
}
|
|
70
|
+
export interface ChatSummary {
|
|
71
|
+
id: string;
|
|
72
|
+
title: string;
|
|
73
|
+
head: string;
|
|
74
|
+
status: ChatStatus;
|
|
75
|
+
updatedAt: number;
|
|
76
|
+
}
|
|
77
|
+
export interface FileEntry {
|
|
78
|
+
name: string;
|
|
79
|
+
path: string;
|
|
80
|
+
directory: boolean;
|
|
81
|
+
}
|
|
82
|
+
export type WorkbenchResult =
|
|
83
|
+
| { kind: "task-origin"; taskId: string; conversationId: string | null }
|
|
84
|
+
| { kind: "conversations"; entries: ChatSummary[] }
|
|
85
|
+
| {
|
|
86
|
+
kind: "conversation";
|
|
87
|
+
summary: ChatSummary | null;
|
|
88
|
+
turns: ChatTurn[];
|
|
89
|
+
before: string | null;
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
kind: "files" | "directories";
|
|
93
|
+
path: string;
|
|
94
|
+
parent: string | null;
|
|
95
|
+
entries: FileEntry[];
|
|
96
|
+
limited: boolean;
|
|
97
|
+
}
|
|
98
|
+
| { kind: "file"; path: string; text: string; limited: boolean }
|
|
99
|
+
| { kind: "projects"; entries: { path: string; current: boolean }[] }
|
|
100
|
+
| { kind: "accepted"; commandId: string }
|
|
101
|
+
| { kind: "project"; commandId: string; path: string; link: string };
|
|
102
|
+
export interface WorkbenchPort {
|
|
103
|
+
read(request: WorkbenchRead): WorkbenchResult;
|
|
104
|
+
command(
|
|
105
|
+
request: WorkbenchCommand,
|
|
106
|
+
): WorkbenchResult | Promise<WorkbenchResult>;
|
|
107
|
+
}
|
|
108
|
+
const statuses = [
|
|
109
|
+
"queued",
|
|
110
|
+
"running",
|
|
111
|
+
"completed",
|
|
112
|
+
"failed",
|
|
113
|
+
"canceled",
|
|
114
|
+
"timeout",
|
|
115
|
+
"unknown",
|
|
116
|
+
] as const;
|
|
117
|
+
export function decodeWorkbenchRead(v: unknown): WorkbenchRead {
|
|
118
|
+
const kind = Object.getOwnPropertyDescriptor(v ?? {}, "kind")?.value;
|
|
119
|
+
switch (kind) {
|
|
120
|
+
case "conversations":
|
|
121
|
+
case "projects":
|
|
122
|
+
closed(v, ["kind"]);
|
|
123
|
+
return { kind };
|
|
124
|
+
case "task-origin": {
|
|
125
|
+
const o = closed(v, ["kind", "taskId"]);
|
|
126
|
+
return { kind, taskId: decodeId(o.taskId) };
|
|
127
|
+
}
|
|
128
|
+
case "conversation": {
|
|
129
|
+
const o = closed(v, ["kind", "id", "before"]);
|
|
130
|
+
return { kind, id: decodeId(o.id), before: nullable(o.before, decodeId) };
|
|
131
|
+
}
|
|
132
|
+
default: {
|
|
133
|
+
const o = closed(v, ["kind", "path"]);
|
|
134
|
+
return {
|
|
135
|
+
kind: oneOf(o.kind, ["files", "file", "directories"]),
|
|
136
|
+
path: text(o.path, 4096),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function decodeWorkbenchCommand(v: unknown): WorkbenchCommand {
|
|
142
|
+
const kind = Object.getOwnPropertyDescriptor(v ?? {}, "kind")?.value;
|
|
143
|
+
if (kind === "link-task") {
|
|
144
|
+
const o = closed(v, [
|
|
145
|
+
"kind",
|
|
146
|
+
"commandId",
|
|
147
|
+
"epoch",
|
|
148
|
+
"taskId",
|
|
149
|
+
"conversationId",
|
|
150
|
+
]);
|
|
151
|
+
return {
|
|
152
|
+
kind,
|
|
153
|
+
commandId: decodeId(o.commandId),
|
|
154
|
+
epoch: decodeId(o.epoch),
|
|
155
|
+
taskId: decodeId(o.taskId),
|
|
156
|
+
conversationId: decodeId(o.conversationId),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (kind === "send") {
|
|
160
|
+
const o = closed(v, [
|
|
161
|
+
"kind",
|
|
162
|
+
"commandId",
|
|
163
|
+
"conversationId",
|
|
164
|
+
"head",
|
|
165
|
+
"text",
|
|
166
|
+
"model",
|
|
167
|
+
]);
|
|
168
|
+
return {
|
|
169
|
+
kind,
|
|
170
|
+
commandId: decodeId(o.commandId),
|
|
171
|
+
conversationId: decodeId(o.conversationId),
|
|
172
|
+
head: nullable(o.head, decodeId),
|
|
173
|
+
text: text(o.text, 16 * 1024, true),
|
|
174
|
+
model: decodeChoiceVersion(o.model),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (kind === "interrupt") {
|
|
178
|
+
const o = closed(v, [
|
|
179
|
+
"kind",
|
|
180
|
+
"commandId",
|
|
181
|
+
"conversationId",
|
|
182
|
+
"turnId",
|
|
183
|
+
"epoch",
|
|
184
|
+
]);
|
|
185
|
+
return {
|
|
186
|
+
kind,
|
|
187
|
+
commandId: decodeId(o.commandId),
|
|
188
|
+
conversationId: decodeId(o.conversationId),
|
|
189
|
+
turnId: decodeId(o.turnId),
|
|
190
|
+
epoch: decodeId(o.epoch),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const o = closed(v, ["kind", "commandId", "path", "epoch"]);
|
|
194
|
+
return {
|
|
195
|
+
kind: oneOf(o.kind, ["open-project"]),
|
|
196
|
+
commandId: decodeId(o.commandId),
|
|
197
|
+
path: text(o.path, 4096, true),
|
|
198
|
+
epoch: decodeId(o.epoch),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export function decodeChatTurn(v: unknown): ChatTurn {
|
|
202
|
+
const o = closed(v, ["id", "epoch", "status", "items", "notice"]);
|
|
203
|
+
return {
|
|
204
|
+
id: decodeId(o.id),
|
|
205
|
+
epoch: decodeId(o.epoch),
|
|
206
|
+
status: oneOf(o.status, statuses),
|
|
207
|
+
notice: text(o.notice, 1024),
|
|
208
|
+
items: list(o.items, 256, (v) => {
|
|
209
|
+
const i = closed(v, ["id", "role", "text", "name", "args", "error"]);
|
|
210
|
+
return {
|
|
211
|
+
id: text(i.id, 160, true),
|
|
212
|
+
role: oneOf(i.role, ["user", "assistant", "tool"]),
|
|
213
|
+
text: text(i.text, 256 * 1024),
|
|
214
|
+
name: text(i.name, 128),
|
|
215
|
+
args: text(i.args, 64 * 1024),
|
|
216
|
+
error: bool(i.error),
|
|
217
|
+
};
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export function decodeChatSummary(v: unknown): ChatSummary {
|
|
222
|
+
const o = closed(v, ["id", "title", "head", "status", "updatedAt"]);
|
|
223
|
+
return {
|
|
224
|
+
id: decodeId(o.id),
|
|
225
|
+
title: text(o.title, 512, true),
|
|
226
|
+
head: decodeId(o.head),
|
|
227
|
+
status: oneOf(o.status, statuses),
|
|
228
|
+
updatedAt: integer(o.updatedAt, 0),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
export function decodeWorkbenchResult(v: unknown): WorkbenchResult {
|
|
232
|
+
// A complete finite reply fits the existing page ceiling. No arbitrary file body.
|
|
233
|
+
canonicalJson(v, 512 * 1024);
|
|
234
|
+
const kind = Object.getOwnPropertyDescriptor(v ?? {}, "kind")?.value;
|
|
235
|
+
if (kind === "task-origin") {
|
|
236
|
+
const o = closed(v, ["kind", "taskId", "conversationId"]);
|
|
237
|
+
return {
|
|
238
|
+
kind,
|
|
239
|
+
taskId: decodeId(o.taskId),
|
|
240
|
+
conversationId: nullable(o.conversationId, decodeId),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (kind === "conversations") {
|
|
244
|
+
const o = closed(v, ["kind", "entries"]);
|
|
245
|
+
return { kind, entries: list(o.entries, 200, decodeChatSummary) };
|
|
246
|
+
}
|
|
247
|
+
if (kind === "conversation") {
|
|
248
|
+
const o = closed(v, ["kind", "summary", "turns", "before"]);
|
|
249
|
+
return {
|
|
250
|
+
kind,
|
|
251
|
+
summary: nullable(o.summary, decodeChatSummary),
|
|
252
|
+
turns: list(o.turns, 20, decodeChatTurn),
|
|
253
|
+
before: nullable(o.before, decodeId),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (kind === "files" || kind === "directories") {
|
|
257
|
+
const o = closed(v, ["kind", "path", "parent", "entries", "limited"]);
|
|
258
|
+
return {
|
|
259
|
+
kind,
|
|
260
|
+
path: text(o.path, 4096),
|
|
261
|
+
parent: nullable(o.parent, (p) => text(p, 4096)),
|
|
262
|
+
limited: bool(o.limited),
|
|
263
|
+
entries: list(o.entries, 200, (v) => {
|
|
264
|
+
const e = closed(v, ["name", "path", "directory"]);
|
|
265
|
+
return {
|
|
266
|
+
name: text(e.name, 1024, true),
|
|
267
|
+
path: text(e.path, 4096, true),
|
|
268
|
+
directory: bool(e.directory),
|
|
269
|
+
};
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (kind === "file") {
|
|
274
|
+
const o = closed(v, ["kind", "path", "text", "limited"]);
|
|
275
|
+
return {
|
|
276
|
+
kind,
|
|
277
|
+
path: text(o.path, 4096),
|
|
278
|
+
text: text(o.text, 256 * 1024),
|
|
279
|
+
limited: bool(o.limited),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (kind === "projects") {
|
|
283
|
+
const o = closed(v, ["kind", "entries"]);
|
|
284
|
+
return {
|
|
285
|
+
kind,
|
|
286
|
+
entries: list(o.entries, 128, (v) => {
|
|
287
|
+
const e = closed(v, ["path", "current"]);
|
|
288
|
+
return { path: text(e.path, 4096, true), current: bool(e.current) };
|
|
289
|
+
}),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (kind === "accepted") {
|
|
293
|
+
const o = closed(v, ["kind", "commandId"]);
|
|
294
|
+
return { kind, commandId: decodeId(o.commandId) };
|
|
295
|
+
}
|
|
296
|
+
const o = closed(v, ["kind", "commandId", "path", "link"]);
|
|
297
|
+
return {
|
|
298
|
+
kind: oneOf(o.kind, ["project"]),
|
|
299
|
+
commandId: decodeId(o.commandId),
|
|
300
|
+
path: text(o.path, 4096, true),
|
|
301
|
+
link: text(o.link, 8192, true),
|
|
302
|
+
};
|
|
303
|
+
}
|