@tea-agent/loop-agent 0.33.6 → 0.33.7-beta.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/CHANGELOG.md +23 -23
- package/dist/application/task-lifecycle/advance.js +4 -254
- package/dist/application/task-lifecycle/gates.js +0 -50
- package/dist/application/task-lifecycle/observe.js +2 -11
- package/dist/commands/init-upgrade.js +1 -32
- package/dist/commands/init.js +3 -94
- package/dist/executors/shell-executor.js +91 -4
- package/dist/executors/shell-write-guard.js +8 -26
- package/dist/shared/operator/capabilities.js +42 -72
- package/dist/task/source-prepare/index.js +0 -2
- package/dist/task/source-prepare/parse-intent.js +10 -58
- package/dist/task/source-prepare/prepare.js +16 -180
- package/dist/task/source-prepare/reference-integrity.js +2 -18
- package/dist/worker/console/app-data.js +0 -2
- package/dist/worker/console/chat/chat-event-store.js +25 -190
- package/dist/worker/console/chat/instruction-skills.js +217 -0
- package/dist/worker/console/chat/pi-console-config.js +32 -250
- package/dist/worker/console/chat/pi-runtime.js +71 -625
- package/dist/worker/console/chat/resource-loader.js +4 -5
- package/dist/worker/console/chat/routes.js +146 -324
- package/dist/worker/console/chat/runtime-context.js +12 -48
- package/dist/worker/console/chat/runtime-selection.js +0 -59
- package/dist/worker/console/chat/shortcuts.js +0 -1
- package/dist/worker/console/chat/tool-adapter.js +3 -9
- package/dist/worker/console/chat/tools.js +1 -5
- package/dist/worker/console/operator-actions.js +68 -559
- package/dist/worker/console/server.js +15 -8
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
- package/dist/worker/console/static/assets/index-CteJFFL2.js +29 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +8 -45
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -9
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -16
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +184 -210
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +5 -49
- package/dist/worker/console/static-src/operator-chat/useComposer.js +0 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +74 -225
- package/dist/worker/delivery/final-verification.js +5 -13
- package/dist/worker/delivery/package.js +19 -31
- package/dist/worker/delivery/verification-bundle.js +4 -6
- package/dist/worker/observe/static/operator-chrome.css +2 -5
- package/dist/worker/observe/static/operator-chrome.js +1 -6
- package/dist/worker/observe/static/styles.css +9 -39
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +462 -33
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +25 -1
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +269 -82
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/frontend-worktree-diff.js +27 -12
- package/dist/workflows/dag/init-hybrid.js +46 -34
- package/dist/workflows/dag/types.js +7 -0
- package/dist/workflows/dag/workspace-checkpoint.js +27 -8
- package/docs/templates/backend-test-dag.json +32 -29
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -3
- package/skills/loop-agent/references/source-and-plan-practice.md +0 -13
- package/skills/loop-agent/references/task-workflow.md +0 -4
- package/dist/shared/resilient-git.js +0 -133
- package/dist/task/source-prepare/artifact-meta.js +0 -137
- package/dist/task/source-prepare/semantic-intake.js +0 -404
- package/dist/worker/console/dag-execution-receipt.js +0 -380
- package/dist/worker/console/static/assets/index-BUOLppPr.js +0 -28
- package/dist/worker/console/static/assets/index-C1KzazY5.css +0 -1
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +0 -257
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +0 -196
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* denied. Interview/Official loaders are unchanged.
|
|
9
9
|
*
|
|
10
10
|
* Note: this adapter is NOT the SDK's own ResourceLoader (that is
|
|
11
|
-
* DefaultResourceLoader, created inside createAgentSessionServices with
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* surface used for capabilities reporting and red-team checks.
|
|
11
|
+
* DefaultResourceLoader, created inside createAgentSessionServices with
|
|
12
|
+
* noContextFiles/noSkills/noExtensions). This adapter is the operator-chat
|
|
13
|
+
* declarative surface used for capabilities reporting and red-team checks.
|
|
15
14
|
* The three-gate authorization (authorizeOperatorChatTool) is the actual
|
|
16
15
|
* enforcement; this adapter describes what that gate admits.
|
|
17
16
|
*/
|
|
@@ -30,7 +29,7 @@ export const OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS = Object.freeze([
|
|
|
30
29
|
export function createOperatorChatResourceLoader() {
|
|
31
30
|
let active = new Set();
|
|
32
31
|
return {
|
|
33
|
-
mode: "operator-chat-
|
|
32
|
+
mode: "operator-chat-closed",
|
|
34
33
|
allowedToolIds: OPERATOR_CHAT_ALLOWED_TOOLS,
|
|
35
34
|
deniedToolIds: OPERATOR_CHAT_DENIED_TOOLS,
|
|
36
35
|
deniedOperatorActions: OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS,
|
|
@@ -6,22 +6,24 @@
|
|
|
6
6
|
* GET /sessions/:id — fetch session metadata + messages
|
|
7
7
|
* POST /sessions/:id/prompt — SSE stream: send a prompt, stream events
|
|
8
8
|
* GET /sessions/:id/events — SSE stream: (re)connect to live events
|
|
9
|
-
* GET /capabilities — Chat tool surface
|
|
9
|
+
* GET /capabilities — Chat tool surface + instruction skills
|
|
10
10
|
* DELETE /sessions/:id — dispose a Chat session
|
|
11
11
|
*
|
|
12
12
|
* Mutation routes go through the Console mutation gate (boot token + Origin).
|
|
13
13
|
* Read routes (GET capabilities / sessions) are open to loopback.
|
|
14
14
|
*/
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
15
16
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
|
|
17
|
+
import { OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS, OPERATOR_CHAT_DENIED_INSTRUCTION_SKILLS, OPERATOR_CHAT_WORKFLOW_CONDITIONAL_SKILLS, composeInstructionSkillsPrompt, loadOperatorChatInstructionSkills, } from "./instruction-skills.js";
|
|
16
18
|
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
17
19
|
import { projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
|
|
18
|
-
import { THINKING_LEVELS,
|
|
20
|
+
import { THINKING_LEVELS, composeOperatorChatSystemPrompt, } from "./pi-runtime.js";
|
|
19
21
|
import { ComposerDraftStore } from "./composer-draft-store.js";
|
|
20
22
|
import { walkRepoFiles } from "./repo-walk.js";
|
|
21
23
|
import { isSensitivePath, scrubSecrets } from "./explore-tools.js";
|
|
22
24
|
import { listRepoDirectory, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
|
|
23
25
|
import { projectRuntimeContext } from "./runtime-context.js";
|
|
24
|
-
import { patchModelsConfig, readModelsConfig, readPackageInventory, } from "./pi-console-config.js";
|
|
26
|
+
import { patchModelsConfig, readModelsConfig, readPackageInventory, readSkillPreferences, writeSkillPreferences, } from "./pi-console-config.js";
|
|
25
27
|
import { ChatInterviewAdapter, projectChatInterviewState, } from "./interview-adapter.js";
|
|
26
28
|
import { classifyChatCompactionFailure } from "./compaction-errors.js";
|
|
27
29
|
import { evaluateMutationGate, isMutationMethod } from "../security.js";
|
|
@@ -31,24 +33,9 @@ import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
|
|
|
31
33
|
import { projectTaskContext } from "./context-panel.js";
|
|
32
34
|
import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
|
|
33
35
|
import { sendJson } from "../routes.js";
|
|
34
|
-
import { openSseResponse, writeSseEvent } from "../operation-sse.js";
|
|
35
|
-
/** Parse Chat's `<epoch>:<seq>` SSE cursor. Legacy `<sessionId>:<seq>` has
|
|
36
|
-
* the same wire shape; its prefix intentionally fails the current process
|
|
37
|
-
* epoch comparison and triggers safe snapshot recovery (M5.1). */
|
|
38
|
-
function parseChatEventCursor(header) {
|
|
39
|
-
if (!header?.trim())
|
|
40
|
-
return { epoch: null, seq: 0 };
|
|
41
|
-
const raw = header.trim();
|
|
42
|
-
const colon = raw.lastIndexOf(":");
|
|
43
|
-
const seqPart = colon >= 0 ? raw.slice(colon + 1) : raw;
|
|
44
|
-
const seq = Number.parseInt(seqPart, 10);
|
|
45
|
-
return {
|
|
46
|
-
epoch: colon > 0 ? raw.slice(0, colon) : null,
|
|
47
|
-
seq: Number.isFinite(seq) && seq >= 0 ? seq : 0,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
36
|
+
import { openSseResponse, parseLastEventId, writeSseEvent, } from "../operation-sse.js";
|
|
50
37
|
function writeChatSse(res, event) {
|
|
51
|
-
// Persisted Chat events carry their own eventId (
|
|
38
|
+
// Persisted Chat events carry their own eventId (<sessionId>:<seq>); write
|
|
52
39
|
// it as the SSE `id:` frame so the browser tracks Last-Event-ID for reconnect.
|
|
53
40
|
if ("seq" in event && event.eventId) {
|
|
54
41
|
res.write(`id: ${event.eventId}\n`);
|
|
@@ -106,32 +93,21 @@ function turnEventToStorePartial(event) {
|
|
|
106
93
|
return undefined;
|
|
107
94
|
}
|
|
108
95
|
}
|
|
109
|
-
/** Guarantee durable terminal
|
|
110
|
-
* emitting
|
|
111
|
-
* error). Without
|
|
112
|
-
*
|
|
113
|
-
*
|
|
96
|
+
/** Guarantee a durable terminal event for a turn that ended without the SDK
|
|
97
|
+
* emitting agent_end (client abort, pre-prompt persistence failure, runtime
|
|
98
|
+
* error). Without it, a reconnect replay stops at agent_start and the UI stays
|
|
99
|
+
* streaming forever. Idempotent: normal settled turns already carry the SDK's
|
|
100
|
+
* agent_end in the ring, so they are skipped. */
|
|
114
101
|
function ensureAgentEndEvent(deps, sessionId, turnId) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
102
|
+
const already = deps.events
|
|
103
|
+
.snapshot(sessionId)
|
|
104
|
+
.some((event) => event.turnId === turnId && event.kind === "agent_end");
|
|
105
|
+
if (!already) {
|
|
118
106
|
deps.events.append(sessionId, turnId, {
|
|
119
107
|
kind: "agent_end",
|
|
120
108
|
data: { willRetry: false },
|
|
121
109
|
});
|
|
122
110
|
}
|
|
123
|
-
// Error/abort paths historically only forced agent_end. After the M4 single
|
|
124
|
-
// stream keeps UI streaming through agent_end for idle grace, a matching
|
|
125
|
-
// agent_settled is required so state reconcile and the applier both finish.
|
|
126
|
-
const hasSettled = deps.events
|
|
127
|
-
.snapshot(sessionId)
|
|
128
|
-
.some((event) => event.turnId === turnId && event.kind === "agent_settled");
|
|
129
|
-
if (!hasSettled) {
|
|
130
|
-
deps.events.append(sessionId, turnId, {
|
|
131
|
-
kind: "agent_settled",
|
|
132
|
-
data: {},
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
111
|
}
|
|
136
112
|
async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
|
|
137
113
|
const chunks = [];
|
|
@@ -222,23 +198,9 @@ function gateMutation(req, deps) {
|
|
|
222
198
|
}
|
|
223
199
|
return { ok: true };
|
|
224
200
|
}
|
|
225
|
-
/**
|
|
226
|
-
* RF-02: map a RuntimeSnapshotUnavailableError (critical snapshot projection
|
|
227
|
-
* failure — model enumeration / system prompt) to 503 PI_RUNTIME_UNAVAILABLE.
|
|
228
|
-
* Returns true when the response was sent; other errors keep their existing
|
|
229
|
-
* 409/500 branches so the mutation/reload semantics never regress.
|
|
230
|
-
*/
|
|
231
|
-
function sendRuntimeUnavailable(res, error) {
|
|
232
|
-
if (!(error instanceof RuntimeSnapshotUnavailableError))
|
|
233
|
-
return false;
|
|
234
|
-
sendJson(res, 503, {
|
|
235
|
-
ok: false,
|
|
236
|
-
error: { code: error.code, message: error.message },
|
|
237
|
-
});
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
201
|
export async function handleChatCapabilities(_req, res, deps) {
|
|
241
202
|
const toolSchemas = buildModelCallableToolSchemas();
|
|
203
|
+
const skills = await loadOperatorChatInstructionSkills(deps.skillsDir);
|
|
242
204
|
sendJson(res, 200, {
|
|
243
205
|
ok: true,
|
|
244
206
|
schemaVersion: 1,
|
|
@@ -246,6 +208,18 @@ export async function handleChatCapabilities(_req, res, deps) {
|
|
|
246
208
|
deniedTools: OPERATOR_CHAT_DENIED_TOOLS,
|
|
247
209
|
deniedOperatorActions: OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS,
|
|
248
210
|
tools: toolSchemas,
|
|
211
|
+
instructionSkills: {
|
|
212
|
+
allowed: OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS,
|
|
213
|
+
loaded: skills.loaded.map((s) => ({
|
|
214
|
+
name: s.name,
|
|
215
|
+
description: s.description,
|
|
216
|
+
charCount: s.charCount,
|
|
217
|
+
})),
|
|
218
|
+
failures: skills.failures,
|
|
219
|
+
skipped: skills.skipped,
|
|
220
|
+
promptFragmentCharCount: composeInstructionSkillsPrompt(skills.loaded)
|
|
221
|
+
.length,
|
|
222
|
+
},
|
|
249
223
|
// ADR 0011: full Pi tools including bash are default-active.
|
|
250
224
|
hasBash: true,
|
|
251
225
|
});
|
|
@@ -302,7 +276,10 @@ export async function handleCreateChatSession(req, res, deps) {
|
|
|
302
276
|
return;
|
|
303
277
|
}
|
|
304
278
|
try {
|
|
305
|
-
|
|
279
|
+
deps.runtime.setDisabledInstructionSkills(await readSkillPreferences(deps.appData));
|
|
280
|
+
const snapshot = snapshotInput === undefined
|
|
281
|
+
? undefined
|
|
282
|
+
: projectCompactSnapshot(snapshotInput);
|
|
306
283
|
const handle = await deps.runtime.createSession({
|
|
307
284
|
...(provider && modelId ? { model: { provider, modelId } } : {}),
|
|
308
285
|
...(snapshot
|
|
@@ -514,7 +491,6 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
|
514
491
|
const lastEvent = snapshot.at(-1);
|
|
515
492
|
sendJson(res, 200, {
|
|
516
493
|
ok: true,
|
|
517
|
-
eventEpoch: deps.events.epoch(sessionId),
|
|
518
494
|
session: {
|
|
519
495
|
...record,
|
|
520
496
|
composerDraft,
|
|
@@ -1049,31 +1025,23 @@ export async function handleChatEventsStream(req, res, deps, sessionId) {
|
|
|
1049
1025
|
}
|
|
1050
1026
|
await deps.operationLinker.recoverSession(sessionId);
|
|
1051
1027
|
openSseResponse(res);
|
|
1052
|
-
// Resume from the client's last seen eventId (
|
|
1053
|
-
// Console boot/legacy cursor has a different prefix and receives snapshot.
|
|
1028
|
+
// Resume from the client's last seen eventId (Last-Event-ID header, T04).
|
|
1054
1029
|
const lastEventIdHeader = req.headers["last-event-id"];
|
|
1055
|
-
const
|
|
1056
|
-
const replay = deps.events.listFrom(sessionId,
|
|
1057
|
-
let lastSeq =
|
|
1030
|
+
const afterSeq = parseLastEventId(Array.isArray(lastEventIdHeader) ? lastEventIdHeader[0] : lastEventIdHeader);
|
|
1031
|
+
const replay = deps.events.listFrom(sessionId, afterSeq);
|
|
1032
|
+
let lastSeq = afterSeq;
|
|
1058
1033
|
if ("error" in replay) {
|
|
1059
1034
|
// Cursor aged out of the bounded ring (T07 fence): tell the client to
|
|
1060
1035
|
// reconcile from a fresh snapshot rather than render a gap as if current.
|
|
1061
|
-
// The frame carries the ring tail (latestEventId) so the client can resume
|
|
1062
|
-
// the replayed snapshot with a fresh Last-Event-ID (recovery cursor).
|
|
1063
|
-
const retained = deps.events.snapshot(sessionId);
|
|
1064
1036
|
writeChatSse(res, {
|
|
1065
1037
|
kind: "reconcile",
|
|
1066
1038
|
sessionId,
|
|
1067
|
-
reason:
|
|
1068
|
-
? "epoch_mismatch"
|
|
1069
|
-
: "cursor_expired",
|
|
1039
|
+
reason: "cursor_expired",
|
|
1070
1040
|
minSeq: replay.minSeq,
|
|
1071
|
-
epoch: replay.epoch,
|
|
1072
1041
|
latestTurnId: deps.events.latestTurnId(sessionId),
|
|
1073
|
-
latestEventId: retained.at(-1)?.eventId ?? null,
|
|
1074
1042
|
});
|
|
1075
1043
|
// Fall back to full retained snapshot so the client can re-render.
|
|
1076
|
-
for (const ev of
|
|
1044
|
+
for (const ev of deps.events.snapshot(sessionId)) {
|
|
1077
1045
|
writeChatSse(res, ev);
|
|
1078
1046
|
lastSeq = Math.max(lastSeq, ev.seq);
|
|
1079
1047
|
}
|
|
@@ -1084,15 +1052,6 @@ export async function handleChatEventsStream(req, res, deps, sessionId) {
|
|
|
1084
1052
|
lastSeq = Math.max(lastSeq, ev.seq);
|
|
1085
1053
|
}
|
|
1086
1054
|
}
|
|
1087
|
-
// Delimit retained replay from follow mode. The client uses this marker to
|
|
1088
|
-
// ensure its sole GET /events consumer is ready before POST /turns (M4.1).
|
|
1089
|
-
const retainedTail = deps.events.snapshot(sessionId).at(-1);
|
|
1090
|
-
writeChatSse(res, {
|
|
1091
|
-
kind: "stream_ready",
|
|
1092
|
-
sessionId,
|
|
1093
|
-
epoch: deps.events.epoch(sessionId),
|
|
1094
|
-
latestEventId: retainedTail?.eventId ?? null,
|
|
1095
|
-
});
|
|
1096
1055
|
let closed = false;
|
|
1097
1056
|
res.once("close", () => {
|
|
1098
1057
|
closed = true;
|
|
@@ -1252,6 +1211,47 @@ async function handlePiConfig(req, res, deps, resource) {
|
|
|
1252
1211
|
});
|
|
1253
1212
|
return;
|
|
1254
1213
|
}
|
|
1214
|
+
if (req.method === "GET") {
|
|
1215
|
+
const disabledNames = await readSkillPreferences(deps.appData);
|
|
1216
|
+
const loaded = await loadOperatorChatInstructionSkills(deps.skillsDir);
|
|
1217
|
+
sendJson(res, 200, {
|
|
1218
|
+
ok: true,
|
|
1219
|
+
data: {
|
|
1220
|
+
allowed: OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS.map((name) => {
|
|
1221
|
+
const skill = loaded.loaded.find((item) => item.name === name);
|
|
1222
|
+
return {
|
|
1223
|
+
name,
|
|
1224
|
+
description: skill?.description ?? "",
|
|
1225
|
+
contentHash: skill
|
|
1226
|
+
? createHash("sha256").update(skill.bodyText).digest("hex")
|
|
1227
|
+
: undefined,
|
|
1228
|
+
};
|
|
1229
|
+
}),
|
|
1230
|
+
conditional: OPERATOR_CHAT_WORKFLOW_CONDITIONAL_SKILLS,
|
|
1231
|
+
denied: OPERATOR_CHAT_DENIED_INSTRUCTION_SKILLS,
|
|
1232
|
+
disabledNames,
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
const gate = gateMutation(req, deps);
|
|
1238
|
+
if (!gate.ok) {
|
|
1239
|
+
sendJson(res, gate.status, {
|
|
1240
|
+
ok: false,
|
|
1241
|
+
error: { code: gate.code, message: gate.message },
|
|
1242
|
+
});
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const body = await readJsonBody(req);
|
|
1246
|
+
const disabledNames = await writeSkillPreferences(deps.appData, Array.isArray(body.disabledNames)
|
|
1247
|
+
? body.disabledNames.filter((name) => typeof name === "string")
|
|
1248
|
+
: []);
|
|
1249
|
+
deps.runtime.setDisabledInstructionSkills(disabledNames);
|
|
1250
|
+
sendJson(res, 200, {
|
|
1251
|
+
ok: true,
|
|
1252
|
+
data: { disabledNames, appliesTo: "new-sessions" },
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
1255
|
}
|
|
1256
1256
|
catch (error) {
|
|
1257
1257
|
const failure = error;
|
|
@@ -1279,148 +1279,94 @@ async function handleChatFiles(res, deps, sessionId, query) {
|
|
|
1279
1279
|
.map((path) => ({ path }));
|
|
1280
1280
|
sendJson(res, 200, { ok: true, files });
|
|
1281
1281
|
}
|
|
1282
|
-
|
|
1283
|
-
const
|
|
1284
|
-
if (!
|
|
1285
|
-
sendJson(res,
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
// AC-03: system prompt comes from the REAL Pi session (agent.state.systemPrompt
|
|
1289
|
-
// fallback chain); resources come from that session's ResourceLoader. The raw
|
|
1290
|
-
// prompt stays server-side; the browser only ever receives the scrubbed +
|
|
1291
|
-
// length-limited projection. RF-02: a critical projection failure is 503
|
|
1292
|
-
// PI_RUNTIME_UNAVAILABLE — never a 500 leak or a fake-empty context.
|
|
1293
|
-
let snapshot;
|
|
1294
|
-
try {
|
|
1295
|
-
snapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
|
|
1296
|
-
includeRaw: true,
|
|
1297
|
-
});
|
|
1298
|
-
}
|
|
1299
|
-
catch (error) {
|
|
1300
|
-
if (sendRuntimeUnavailable(res, error))
|
|
1301
|
-
return;
|
|
1302
|
-
throw error;
|
|
1303
|
-
}
|
|
1304
|
-
if (!snapshot) {
|
|
1305
|
-
sendJson(res, 503, {
|
|
1306
|
-
ok: false,
|
|
1307
|
-
error: {
|
|
1308
|
-
code: "PI_RUNTIME_UNAVAILABLE",
|
|
1309
|
-
message: `chat session not active: ${sessionId}`,
|
|
1310
|
-
},
|
|
1311
|
-
});
|
|
1282
|
+
async function handleRuntimeContext(res, deps, sessionId) {
|
|
1283
|
+
const record = await deps.store.get(sessionId);
|
|
1284
|
+
if (!record) {
|
|
1285
|
+
sendJson(res, 404, { ok: false });
|
|
1312
1286
|
return;
|
|
1313
1287
|
}
|
|
1288
|
+
const skills = await loadOperatorChatInstructionSkills(deps.skillsDir);
|
|
1289
|
+
const selection = deps.runtime.getRuntimeSelection(sessionId);
|
|
1290
|
+
const skillFragment = composeInstructionSkillsPrompt(skills.loaded);
|
|
1291
|
+
const systemPrompt = composeOperatorChatSystemPrompt({ skillFragment });
|
|
1314
1292
|
sendJson(res, 200, {
|
|
1315
1293
|
ok: true,
|
|
1316
1294
|
context: projectRuntimeContext({
|
|
1317
|
-
systemPrompt
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
: undefined,
|
|
1324
|
-
thinkingLevel: snapshot.selection.thinkingSelection === "auto"
|
|
1325
|
-
? snapshot.selection.effectiveThinkingLevel
|
|
1326
|
-
: snapshot.selection.thinkingSelection,
|
|
1327
|
-
activeTools: snapshot.activeTools,
|
|
1328
|
-
contextFiles: snapshot.contextFiles,
|
|
1329
|
-
skills: snapshot.skills,
|
|
1330
|
-
extensions: snapshot.extensions,
|
|
1331
|
-
packages: snapshot.packages.map((pkg) => ({
|
|
1332
|
-
name: pkg.spec,
|
|
1333
|
-
source: pkg.spec,
|
|
1334
|
-
scope: pkg.scope,
|
|
1335
|
-
enabled: pkg.enabled,
|
|
1336
|
-
...(pkg.version ? { version: pkg.version } : {}),
|
|
1337
|
-
...(pkg.path ? { path: pkg.path } : {}),
|
|
1338
|
-
...(pkg.cwd ? { cwd: pkg.cwd } : {}),
|
|
1339
|
-
resolved: pkg.resolved,
|
|
1340
|
-
diagnostics: pkg.diagnostics,
|
|
1295
|
+
systemPrompt,
|
|
1296
|
+
skills: skills.loaded.map((skill) => ({
|
|
1297
|
+
name: skill.name,
|
|
1298
|
+
description: skill.description,
|
|
1299
|
+
charCount: skill.charCount,
|
|
1300
|
+
bodyText: skill.bodyText,
|
|
1341
1301
|
})),
|
|
1342
|
-
|
|
1302
|
+
model: selection.model ??
|
|
1303
|
+
(record.modelProvider && record.modelId
|
|
1304
|
+
? { provider: record.modelProvider, modelId: record.modelId }
|
|
1305
|
+
: undefined),
|
|
1306
|
+
thinkingLevel: selection.thinkingLevel ?? record.thinkingLevel,
|
|
1307
|
+
activeTools: selection.activeTools,
|
|
1343
1308
|
}),
|
|
1344
1309
|
});
|
|
1345
1310
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
export async function handleModels(req, res, deps, sessionId) {
|
|
1349
|
-
// AC-01/AC-06: models (and model/thinking mutation) requests reuse the same
|
|
1350
|
-
// activation path. A missing/reopen-failed runtime is a STRUCTURED error —
|
|
1351
|
-
// never a fabricated empty model list.
|
|
1352
|
-
const ensured = await ensureActiveSession(deps, sessionId);
|
|
1353
|
-
if (!ensured.ok) {
|
|
1354
|
-
sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
|
|
1311
|
+
async function handleModels(req, res, deps, sessionId) {
|
|
1312
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1355
1313
|
return;
|
|
1356
|
-
}
|
|
1357
1314
|
try {
|
|
1358
1315
|
if ((req.method ?? "GET").toUpperCase() === "GET") {
|
|
1359
|
-
let models;
|
|
1360
|
-
try {
|
|
1361
|
-
models = await deps.runtime.listModels(sessionId);
|
|
1362
|
-
}
|
|
1363
|
-
catch (error) {
|
|
1364
|
-
// AC-06: a model-list failure is a STRUCTURED runtime error — never
|
|
1365
|
-
// folded into a fake empty list.
|
|
1366
|
-
sendJson(res, 503, {
|
|
1367
|
-
ok: false,
|
|
1368
|
-
error: {
|
|
1369
|
-
code: "PI_RUNTIME_UNAVAILABLE",
|
|
1370
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1371
|
-
},
|
|
1372
|
-
});
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
1316
|
sendJson(res, 200, {
|
|
1376
1317
|
ok: true,
|
|
1377
|
-
models:
|
|
1378
|
-
provider: model.provider,
|
|
1379
|
-
id: model.id ?? model.modelId ?? "",
|
|
1380
|
-
...(model.name ? { name: model.name } : {}),
|
|
1381
|
-
hasCredentials: model.hasCredentials,
|
|
1382
|
-
})),
|
|
1318
|
+
models: await deps.runtime.listModels(sessionId),
|
|
1383
1319
|
thinkingLevels: THINKING_LEVELS,
|
|
1384
1320
|
});
|
|
1385
1321
|
return;
|
|
1386
1322
|
}
|
|
1387
1323
|
const gate = gateMutation(req, deps);
|
|
1388
1324
|
if (!gate.ok) {
|
|
1389
|
-
sendJson(res, gate.status, {
|
|
1325
|
+
sendJson(res, gate.status, {
|
|
1326
|
+
ok: false,
|
|
1327
|
+
error: { code: gate.code, message: gate.message },
|
|
1328
|
+
});
|
|
1390
1329
|
return;
|
|
1391
1330
|
}
|
|
1392
1331
|
const body = await readJsonBody(req);
|
|
1393
1332
|
let updated;
|
|
1394
1333
|
if (typeof body.provider === "string" && typeof body.modelId === "string") {
|
|
1395
|
-
const model = await deps.runtime.applyModel(sessionId, {
|
|
1396
|
-
|
|
1334
|
+
const model = await deps.runtime.applyModel(sessionId, {
|
|
1335
|
+
provider: body.provider,
|
|
1336
|
+
modelId: body.modelId,
|
|
1337
|
+
});
|
|
1338
|
+
updated = await deps.store.setRuntimeSelection(sessionId, {
|
|
1339
|
+
modelProvider: model.provider,
|
|
1340
|
+
modelId: model.modelId,
|
|
1341
|
+
});
|
|
1397
1342
|
}
|
|
1398
|
-
else if (typeof body.thinkingLevel === "string" &&
|
|
1343
|
+
else if (typeof body.thinkingLevel === "string" &&
|
|
1344
|
+
THINKING_LEVELS.includes(body.thinkingLevel)) {
|
|
1399
1345
|
const thinkingLevel = deps.runtime.applyThinkingLevel(sessionId, body.thinkingLevel);
|
|
1400
|
-
updated = await deps.store.setRuntimeSelection(sessionId,
|
|
1346
|
+
updated = await deps.store.setRuntimeSelection(sessionId, {
|
|
1347
|
+
thinkingLevel,
|
|
1348
|
+
});
|
|
1401
1349
|
}
|
|
1402
1350
|
else {
|
|
1403
|
-
sendJson(res, 400, {
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
// Design §5: mutations return the fresh snapshot so the browser can
|
|
1407
|
-
// atomically replace its state (revision increments server-side).
|
|
1408
|
-
// RF-02: the mutation may already be applied server-side, but a failed
|
|
1409
|
-
// snapshot read must surface as 503 PI_RUNTIME_UNAVAILABLE — NOT be
|
|
1410
|
-
// folded into a 409 MODEL_SWITCH_FAILED (the frontend refresh path
|
|
1411
|
-
// re-reads the snapshot to converge on the applied selection).
|
|
1412
|
-
const snapshot = await deps.runtime.getRuntimeSnapshot(sessionId);
|
|
1413
|
-
sendJson(res, 200, { ok: true, session: updated, snapshot });
|
|
1414
|
-
}
|
|
1415
|
-
catch (error) {
|
|
1416
|
-
if (error instanceof RuntimeSnapshotUnavailableError) {
|
|
1417
|
-
sendJson(res, 503, {
|
|
1351
|
+
sendJson(res, 400, {
|
|
1418
1352
|
ok: false,
|
|
1419
|
-
error: {
|
|
1353
|
+
error: {
|
|
1354
|
+
code: "INVALID_INPUT",
|
|
1355
|
+
message: "provider/modelId or thinkingLevel is required",
|
|
1356
|
+
},
|
|
1420
1357
|
});
|
|
1421
1358
|
return;
|
|
1422
1359
|
}
|
|
1423
|
-
sendJson(res,
|
|
1360
|
+
sendJson(res, 200, { ok: true, session: updated });
|
|
1361
|
+
}
|
|
1362
|
+
catch (error) {
|
|
1363
|
+
sendJson(res, 409, {
|
|
1364
|
+
ok: false,
|
|
1365
|
+
error: {
|
|
1366
|
+
code: "MODEL_SWITCH_FAILED",
|
|
1367
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1368
|
+
},
|
|
1369
|
+
});
|
|
1424
1370
|
}
|
|
1425
1371
|
}
|
|
1426
1372
|
async function requireChatSession(res, deps, sessionId) {
|
|
@@ -1435,132 +1381,6 @@ async function requireChatSession(res, deps, sessionId) {
|
|
|
1435
1381
|
});
|
|
1436
1382
|
return false;
|
|
1437
1383
|
}
|
|
1438
|
-
/**
|
|
1439
|
-
* Unified session activation (design §2.1 / AC-01): models, runtime snapshot,
|
|
1440
|
-
* reload and model/thinking mutation requests all flow through this path.
|
|
1441
|
-
*
|
|
1442
|
-
* - runtime already holds the session → ok;
|
|
1443
|
-
* - only a persisted record exists → reopen from record.sessionFile;
|
|
1444
|
-
* - no record / missing session file → 404 PI_SESSION_NOT_FOUND;
|
|
1445
|
-
* - SDK reopen failure → 503 PI_RUNTIME_UNAVAILABLE.
|
|
1446
|
-
*
|
|
1447
|
-
* Never fabricates a fresh session or an empty model list.
|
|
1448
|
-
*/
|
|
1449
|
-
export async function ensureActiveSession(deps, sessionId) {
|
|
1450
|
-
if (deps.runtime.hasSession(sessionId))
|
|
1451
|
-
return { ok: true };
|
|
1452
|
-
const record = await deps.store.get(sessionId);
|
|
1453
|
-
if (!record) {
|
|
1454
|
-
return {
|
|
1455
|
-
ok: false,
|
|
1456
|
-
status: 404,
|
|
1457
|
-
code: "PI_SESSION_NOT_FOUND",
|
|
1458
|
-
message: `chat session not found: ${sessionId}`,
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
|
-
const reopened = await deps.runtime.reopenSession({
|
|
1462
|
-
sessionId,
|
|
1463
|
-
sessionFile: record.sessionFile,
|
|
1464
|
-
...(record.modelProvider && record.modelId
|
|
1465
|
-
? { model: { provider: record.modelProvider, modelId: record.modelId } }
|
|
1466
|
-
: {}),
|
|
1467
|
-
});
|
|
1468
|
-
if (!reopened.ok) {
|
|
1469
|
-
return reopened.code === "NO_SESSION_FILE"
|
|
1470
|
-
? {
|
|
1471
|
-
ok: false,
|
|
1472
|
-
status: 404,
|
|
1473
|
-
code: "PI_SESSION_NOT_FOUND",
|
|
1474
|
-
message: reopened.message,
|
|
1475
|
-
}
|
|
1476
|
-
: {
|
|
1477
|
-
ok: false,
|
|
1478
|
-
status: 503,
|
|
1479
|
-
code: "PI_RUNTIME_UNAVAILABLE",
|
|
1480
|
-
message: reopened.message,
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
return { ok: true };
|
|
1484
|
-
}
|
|
1485
|
-
/**
|
|
1486
|
-
* POST /sessions/:id/reload (AC-05 / AC-06): official session.reload() with
|
|
1487
|
-
* structured busy/failure semantics. Never aborts, never silently queues.
|
|
1488
|
-
*/
|
|
1489
|
-
export async function handleReloadSession(req, res, deps, sessionId) {
|
|
1490
|
-
const gate = gateMutation(req, deps);
|
|
1491
|
-
if (!gate.ok) {
|
|
1492
|
-
sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
|
|
1493
|
-
return;
|
|
1494
|
-
}
|
|
1495
|
-
const ensured = await ensureActiveSession(deps, sessionId);
|
|
1496
|
-
if (!ensured.ok) {
|
|
1497
|
-
sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
|
|
1498
|
-
return;
|
|
1499
|
-
}
|
|
1500
|
-
const result = await deps.runtime.reloadSession(sessionId);
|
|
1501
|
-
if (!result.ok) {
|
|
1502
|
-
const status = result.code === "PI_SESSION_NOT_FOUND"
|
|
1503
|
-
? 404
|
|
1504
|
-
: result.code === "PI_SESSION_BUSY"
|
|
1505
|
-
? 409
|
|
1506
|
-
: 500;
|
|
1507
|
-
sendJson(res, status, { ok: false, error: { code: result.code, message: result.message } });
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
// RF-02: the reload succeeded, but a failed snapshot read is still a
|
|
1511
|
-
// structured 503 — never a 200 with fake-empty resources.
|
|
1512
|
-
let snapshot;
|
|
1513
|
-
try {
|
|
1514
|
-
snapshot = await deps.runtime.getRuntimeSnapshot(sessionId);
|
|
1515
|
-
}
|
|
1516
|
-
catch (error) {
|
|
1517
|
-
if (sendRuntimeUnavailable(res, error))
|
|
1518
|
-
return;
|
|
1519
|
-
throw error;
|
|
1520
|
-
}
|
|
1521
|
-
sendJson(res, 200, {
|
|
1522
|
-
ok: true,
|
|
1523
|
-
revision: result.revision,
|
|
1524
|
-
snapshot,
|
|
1525
|
-
resourceCounts: snapshot
|
|
1526
|
-
? {
|
|
1527
|
-
skills: snapshot.skills.length,
|
|
1528
|
-
extensions: snapshot.extensions.length,
|
|
1529
|
-
packages: snapshot.packages.length,
|
|
1530
|
-
contextFiles: snapshot.contextFiles.length,
|
|
1531
|
-
}
|
|
1532
|
-
: undefined,
|
|
1533
|
-
});
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
1536
|
-
* GET /sessions/:id/runtime-snapshot (AC-01): the unified live snapshot.
|
|
1537
|
-
*/
|
|
1538
|
-
export async function handleRuntimeSnapshot(_res, res, deps, sessionId) {
|
|
1539
|
-
const ensured = await ensureActiveSession(deps, sessionId);
|
|
1540
|
-
if (!ensured.ok) {
|
|
1541
|
-
sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
|
-
// RF-02: a critical snapshot projection failure is 503 PI_RUNTIME_UNAVAILABLE
|
|
1545
|
-
// — never a 200 fake-empty snapshot.
|
|
1546
|
-
let snapshot;
|
|
1547
|
-
try {
|
|
1548
|
-
snapshot = await deps.runtime.getRuntimeSnapshot(sessionId);
|
|
1549
|
-
}
|
|
1550
|
-
catch (error) {
|
|
1551
|
-
if (sendRuntimeUnavailable(res, error))
|
|
1552
|
-
return;
|
|
1553
|
-
throw error;
|
|
1554
|
-
}
|
|
1555
|
-
if (!snapshot) {
|
|
1556
|
-
sendJson(res, 503, {
|
|
1557
|
-
ok: false,
|
|
1558
|
-
error: { code: "PI_RUNTIME_UNAVAILABLE", message: `chat session not active: ${sessionId}` },
|
|
1559
|
-
});
|
|
1560
|
-
return;
|
|
1561
|
-
}
|
|
1562
|
-
sendJson(res, 200, { ok: true, snapshot });
|
|
1563
|
-
}
|
|
1564
1384
|
async function handleLinkWorkspaceOperation(req, res, deps, sessionId) {
|
|
1565
1385
|
const gate = gateMutation(req, deps);
|
|
1566
1386
|
if (!gate.ok) {
|
|
@@ -2333,6 +2153,16 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
2333
2153
|
await handlePiConfig(req, res, deps, "packages");
|
|
2334
2154
|
return true;
|
|
2335
2155
|
}
|
|
2156
|
+
if (pathname === "/api/operator/v1/chat/instruction-skills" &&
|
|
2157
|
+
method === "GET") {
|
|
2158
|
+
await handlePiConfig(req, res, deps, "skills");
|
|
2159
|
+
return true;
|
|
2160
|
+
}
|
|
2161
|
+
if (pathname === "/api/operator/v1/chat/instruction-skills/preferences" &&
|
|
2162
|
+
method === "PUT") {
|
|
2163
|
+
await handlePiConfig(req, res, deps, "skills");
|
|
2164
|
+
return true;
|
|
2165
|
+
}
|
|
2336
2166
|
if (method === "GET" && pathname === "/api/operator/v1/chat/capabilities") {
|
|
2337
2167
|
await handleChatCapabilities(req, res, deps);
|
|
2338
2168
|
return true;
|
|
@@ -2391,7 +2221,7 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
2391
2221
|
await handleMutationHumanGate(req, res, deps, decodeURIComponent(mutationHumanGate[1]));
|
|
2392
2222
|
return true;
|
|
2393
2223
|
}
|
|
2394
|
-
const m4Route = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/(draft|files|runtime-context|
|
|
2224
|
+
const m4Route = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/(draft|files|runtime-context|models|model)$/);
|
|
2395
2225
|
if (m4Route) {
|
|
2396
2226
|
const id = decodeURIComponent(m4Route[1]);
|
|
2397
2227
|
const route = m4Route[2];
|
|
@@ -2407,10 +2237,6 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
2407
2237
|
await handleRuntimeContext(res, deps, id);
|
|
2408
2238
|
return true;
|
|
2409
2239
|
}
|
|
2410
|
-
if (route === "runtime-snapshot" && method === "GET") {
|
|
2411
|
-
await handleRuntimeSnapshot(req, res, deps, id);
|
|
2412
|
-
return true;
|
|
2413
|
-
}
|
|
2414
2240
|
if (route === "models" && method === "GET") {
|
|
2415
2241
|
await handleModels(req, res, deps, id);
|
|
2416
2242
|
return true;
|
|
@@ -2419,10 +2245,6 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
2419
2245
|
await handleModels(req, res, deps, id);
|
|
2420
2246
|
return true;
|
|
2421
2247
|
}
|
|
2422
|
-
if (route === "reload" && method === "POST") {
|
|
2423
|
-
await handleReloadSession(req, res, deps, id);
|
|
2424
|
-
return true;
|
|
2425
|
-
}
|
|
2426
2248
|
}
|
|
2427
2249
|
const sessionMainline = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/branch\/mainline$/);
|
|
2428
2250
|
if (method === "POST" && sessionMainline) {
|