@tea-agent/loop-agent 0.33.6-beta.0 → 0.33.6
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 +29 -4
- package/dist/application/task-lifecycle/advance.js +254 -4
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +72 -42
- package/dist/shared/resilient-git.js +133 -0
- package/dist/task/source-prepare/artifact-meta.js +137 -0
- package/dist/task/source-prepare/index.js +2 -0
- package/dist/task/source-prepare/parse-intent.js +58 -10
- package/dist/task/source-prepare/prepare.js +180 -16
- package/dist/task/source-prepare/reference-integrity.js +18 -2
- package/dist/task/source-prepare/semantic-intake.js +404 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +190 -25
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +625 -71
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +324 -146
- package/dist/worker/console/chat/runtime-context.js +48 -12
- package/dist/worker/console/chat/runtime-selection.js +59 -0
- package/dist/worker/console/chat/shortcuts.js +1 -0
- package/dist/worker/console/chat/tool-adapter.js +9 -3
- package/dist/worker/console/chat/tools.js +5 -1
- package/dist/worker/console/dag-execution-receipt.js +380 -0
- package/dist/worker/console/operator-actions.js +559 -68
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BUOLppPr.js +28 -0
- package/dist/worker/console/static/assets/index-C1KzazY5.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +16 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +210 -184
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
- package/dist/worker/delivery/final-verification.js +13 -5
- package/dist/worker/delivery/package.js +31 -19
- package/dist/worker/delivery/verification-bundle.js +6 -4
- package/dist/worker/observe/static/operator-chrome.css +5 -2
- package/dist/worker/observe/static/operator-chrome.js +6 -1
- package/dist/worker/observe/static/styles.css +39 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +2 -27
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
- package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/init-hybrid.js +12 -20
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/backend-test-dag.json +10 -10
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +3 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
- package/skills/loop-agent/references/task-workflow.md +4 -0
- package/dist/worker/console/chat/instruction-skills.js +0 -217
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
- package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
|
@@ -9,60 +9,64 @@
|
|
|
9
9
|
* - Chat sessions are persisted to a DEDICATED sessionDir (not the user's
|
|
10
10
|
* default ~/.pi/agent/sessions). Naming prefix `operator-chat-` makes them
|
|
11
11
|
* auditable and cleanable (plan Q4 / Q8).
|
|
12
|
-
* - The ResourceLoader
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* - The ResourceLoader uses the SDK's standard OPEN surface (ADR 0012):
|
|
13
|
+
* user/project AGENTS.md context files, user (~/.pi/agent/skills/) and
|
|
14
|
+
* project (.agents/skills/ init projection) skills, and user/project
|
|
15
|
+
* extensions all load via the SDK DefaultResourceLoader; credential/model
|
|
16
|
+
* plane is shared via the same agentDir auth.json/models.json.
|
|
15
17
|
* - Active tools are pinned to the operator-chat surface at session create
|
|
16
18
|
* AND re-pinned before each prompt (three-gate, design §7.5 / ADR 0011):
|
|
17
19
|
* the FULL operator action set PLUS full Pi builtins
|
|
18
|
-
* (read/write/edit/bash/grep/find/ls) and optional safe
|
|
20
|
+
* (read/write/edit/bash/grep/find/ls) and optional safe-*, PLUS extension
|
|
21
|
+
* tools registered by user/project extensions (ADR 0012). Non-Pi write
|
|
19
22
|
* channels (apply_patch/full-tools/shell/coding-chat) stay excluded.
|
|
20
23
|
*
|
|
21
24
|
* The actual SDK calls are injected via `PiSdkBindings` so this module is
|
|
22
25
|
* unit-testable without a live Pi install. Production bindings come from
|
|
23
26
|
* `createDefaultPiSdkBindings()`.
|
|
24
27
|
*/
|
|
25
|
-
import { randomBytes } from "node:crypto";
|
|
28
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
26
29
|
import { access } from "node:fs/promises";
|
|
27
30
|
import path from "node:path";
|
|
28
31
|
import { projectCompactSnapshot, } from "./chat-event-store.js";
|
|
29
32
|
import { extractUsageSample } from "./usage.js";
|
|
30
33
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
|
|
31
34
|
import { createOperatorChatResourceLoader, } from "./resource-loader.js";
|
|
32
|
-
import { loadOperatorChatInstructionSkills, composeInstructionSkillsPrompt, OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS, } from "./instruction-skills.js";
|
|
33
35
|
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
34
36
|
import { resolveDefaultChatModel, } from "./model-resolver.js";
|
|
35
37
|
import { filterActiveInterviewTools } from "../interview/tools.js";
|
|
38
|
+
import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
|
|
36
39
|
/**
|
|
37
40
|
* Base system prompt fragment every General Operator Chat session receives.
|
|
38
41
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* 2026-08-11 (ADR 0013): this is a SHORT runtime responsibility layer only.
|
|
43
|
+
* Project governance (operator-only policy, allowedPaths/forbiddenPaths,
|
|
44
|
+
* writer writeSet, taskKind routing, verification matrix, DAG recovery
|
|
45
|
+
* commands, closeout) lives in the loaded target-project AGENTS.md. The
|
|
46
|
+
* prompt below covers: Operator identity, the real tool surface (operator_*
|
|
47
|
+
* first + Pi tools, obey AGENTS.md), denied non-Pi runtimes, safe-* probes,
|
|
48
|
+
* init/init-upgrade fallback for uninitialized repos, DAG supervision with
|
|
49
|
+
* 15/30/60s adaptive polling (operationGet/status/dagReport/dagDoctor), R2
|
|
50
|
+
* recovery (fresh eligible dagRerunPlan → dagRerun; ineligible → same-task
|
|
51
|
+
* rerun/advance, resume, Worker retry) and G2 bounded writeSet auto-approval.
|
|
47
52
|
*
|
|
48
|
-
*
|
|
53
|
+
* The old Human Gate tutorial paragraphs (prepare + browser Human Gate,
|
|
54
|
+
* "confirmDagConfirmation is a human-only action", the long "Recovery default
|
|
55
|
+
* order" table) are REMOVED — bounded DAGs start and recover autonomously
|
|
56
|
+
* (AC-01: ≤ 1900 chars and ≤ 80% of the 2498-char baseline).
|
|
49
57
|
*/
|
|
50
58
|
export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
51
59
|
"You are the General Operator Chat for loop-agent / agent-worker.",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"You cannot self-confirm a DAG run: confirmDagConfirmation is a human-only action (executed by the browser with a server-signed confirmation token). You may only prepare it via prepareDagConfirmation; the user must confirm in the UI.",
|
|
58
|
-
"Prefer read-only diagnosis (status, doctor, dagReport, inspect, contractShow, read, safe-read, safe-grep, git-status) before mutating.",
|
|
59
|
-
"Recovery default order (save tokens/time; do NOT open a new task-id for provider flake): (1) dagReport/doctor/status — read primaryFailure.nodeId + primaryRecovery; (2) for completed failed|partial_failed standalone runs prefer dagRerunPlan then prepareMutationGate/dagRerun from the failed or safe upstream node (plan → planHash → human gate); (3) paused → approve path / resume, never treat resume as failure retry; (4) only when plan is ineligible (writer/decision/fingerprint) or contract/source truly changed: standaloneTaskRerun or same-task task advance — never invent a fresh task-id without cause; (5) Worker-owned failures use workerTaskRetry, not dagRerun. Never use direct write/edit as a substitute for repair-pi.",
|
|
60
|
+
"Operate through operator_* tools first. Pi read/write/edit/bash/grep/find/ls are available, but obey the loaded repository AGENTS.md. apply_patch, full-tools, shell, and coding-chat are denied alternate runtimes. Prefer safe-read/safe-grep for sensitive probes.",
|
|
61
|
+
"If a repository has no loop-agent harness and the user requests initialization, run `loop-agent init instructions --repo-root .` then `loop-agent init --repo-root . --profile full --merge`; finish the generated setup, init doctor, inspect, docs audit, and quick verification. For updates use `loop-agent init upgrade --repo-root . --json` until stable; use init check-update only for an explicitly read-only request.",
|
|
62
|
+
"After a DAG is started or accepted, do not end on accepted/queued/running or an operationId. Supervise it to a terminal outcome. Poll operationGet, task status, dagReport, and dagDoctor after about 15 seconds on start/change, every 30 seconds during progress, and every 60 seconds after 3 minutes unchanged. Report only meaningful node/rank changes, review/verify/closeout, recovery, liveness concerns, and terminal outcomes.",
|
|
63
|
+
"On failure read primaryFailure, primaryRecovery, and doctor evidence. If meaningful progress exists, wait. Otherwise use a fresh eligible dagRerunPlan and rerun its safe node; when ineligible follow AGENTS.md/runtime recovery for same-task rerun/advance, resume, or Worker retry. Auto-approve only a bounded writeSet inside allowedPaths, outside forbiddenPaths, without broad/destructive risk, and with structured verification.",
|
|
64
|
+
"Continue until success, user stop, or no safe eligible recovery remains because limits, bindings, auth/quota recovery, or required external authorization are exhausted. Never create a new task for a transient failure or replace repair-pi with direct edits.",
|
|
60
65
|
].join("\n");
|
|
61
66
|
/** Compose the inspectable system prompt actually injected into Operator Chat. */
|
|
62
67
|
export function composeOperatorChatSystemPrompt(input) {
|
|
63
68
|
return [
|
|
64
69
|
OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
65
|
-
...(input.skillFragment?.trim() ? [input.skillFragment.trim()] : []),
|
|
66
70
|
...(input.systemPromptSuffix?.trim()
|
|
67
71
|
? [input.systemPromptSuffix.trim()]
|
|
68
72
|
: []),
|
|
@@ -102,6 +106,49 @@ export function OPERATOR_CHAT_ACTIVE_TOOL_NAMES() {
|
|
|
102
106
|
...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
|
|
103
107
|
];
|
|
104
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Non-Pi write / coding channels that must never be activated in Operator
|
|
111
|
+
* Chat even when a user/project extension registers them (ADR 0011 / 0012).
|
|
112
|
+
* Mirrors the NON_PI_WRITE_CHANNELS guard in tools.ts for the Gate 2 pin.
|
|
113
|
+
*/
|
|
114
|
+
const OPERATOR_CHAT_EXTENSION_DENIED_TOOL_IDS = new Set([
|
|
115
|
+
"apply_patch",
|
|
116
|
+
"apply-patch",
|
|
117
|
+
"full-tools",
|
|
118
|
+
"full_tools",
|
|
119
|
+
"coding-chat",
|
|
120
|
+
"coding_chat",
|
|
121
|
+
"shell",
|
|
122
|
+
].map((t) => t.toLowerCase()));
|
|
123
|
+
/**
|
|
124
|
+
* Gate 2 active-tool set for a live session (ADR 0012): the operator-chat
|
|
125
|
+
* baseline (operator actions + Pi builtins + safe explore) PLUS extension
|
|
126
|
+
* tools registered by user/project extensions. SDK setActiveToolsByName is
|
|
127
|
+
* REPLACEMENT-style, so extension tools would be disabled by the fixed
|
|
128
|
+
* baseline; this helper re-adds every registry tool that is not part of the
|
|
129
|
+
* baseline and not on the deny surface (apply_patch/full-tools/shell/
|
|
130
|
+
* coding-chat). Unknown/denied names are never activated; Gate 3 still
|
|
131
|
+
* re-authorizes every tool call per invocation.
|
|
132
|
+
*/
|
|
133
|
+
export function computeOperatorChatActiveToolNames(session) {
|
|
134
|
+
const baseline = OPERATOR_CHAT_ACTIVE_TOOL_NAMES();
|
|
135
|
+
if (!session?.getAllTools)
|
|
136
|
+
return baseline;
|
|
137
|
+
const baselineLower = new Set(baseline.map((t) => t.toLowerCase()));
|
|
138
|
+
const extras = [];
|
|
139
|
+
for (const tool of session.getAllTools()) {
|
|
140
|
+
const name = tool?.name;
|
|
141
|
+
if (!name || typeof name !== "string")
|
|
142
|
+
continue;
|
|
143
|
+
const lower = name.toLowerCase();
|
|
144
|
+
if (baselineLower.has(lower))
|
|
145
|
+
continue;
|
|
146
|
+
if (OPERATOR_CHAT_EXTENSION_DENIED_TOOL_IDS.has(lower))
|
|
147
|
+
continue;
|
|
148
|
+
extras.push(name);
|
|
149
|
+
}
|
|
150
|
+
return extras.length > 0 ? [...baseline, ...extras] : baseline;
|
|
151
|
+
}
|
|
105
152
|
/**
|
|
106
153
|
* Build Pi custom ToolDefinition objects for every whitelisted operator action.
|
|
107
154
|
* Each tool's execute() routes into the operator action dispatcher (Gate 3).
|
|
@@ -382,6 +429,138 @@ export const THINKING_LEVELS = [
|
|
|
382
429
|
"xhigh",
|
|
383
430
|
"max",
|
|
384
431
|
];
|
|
432
|
+
/**
|
|
433
|
+
* Structured critical-failure error for snapshot projection (RF-02). Model
|
|
434
|
+
* enumeration or system prompt projection failures throw this so consuming
|
|
435
|
+
* routes can return 503 PI_RUNTIME_UNAVAILABLE instead of a fake-empty 200.
|
|
436
|
+
* Partial ResourceLoader / package-inventory failures are NOT critical — they
|
|
437
|
+
* keep the 200 snapshot and surface as structured diagnostics instead.
|
|
438
|
+
*/
|
|
439
|
+
export class RuntimeSnapshotUnavailableError extends Error {
|
|
440
|
+
code = "PI_RUNTIME_UNAVAILABLE";
|
|
441
|
+
constructor(message) {
|
|
442
|
+
super(message);
|
|
443
|
+
this.name = "RuntimeSnapshotUnavailableError";
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Browser-safe projection of a live runtime snapshot (design §2.2 / RF-03).
|
|
448
|
+
*
|
|
449
|
+
* Every browser-visible string runs the shared secret-scrub chain
|
|
450
|
+
* (scrubSecrets + ghp_/Bearer redaction via redactRuntimeText) and a fixed
|
|
451
|
+
* per-field length limit: 200 (names/spec/source/origin), 400 (paths/errors/
|
|
452
|
+
* diagnostic messages), 600 (skill descriptions), 80 (package versions);
|
|
453
|
+
* truncation uses the fixed `…[truncated N chars …]` marker. The server-only
|
|
454
|
+
* `raw` prompt and the RAW-based `sha256`/`characters` pass through untouched,
|
|
455
|
+
* and real empty lists stay empty.
|
|
456
|
+
*
|
|
457
|
+
* Pure + deterministic and exported so the route harness reuses the exact
|
|
458
|
+
* production implementation (same code path as getRuntimeSnapshot).
|
|
459
|
+
*/
|
|
460
|
+
export function projectRuntimeSnapshot(input) {
|
|
461
|
+
const field = (value, max) => value === undefined ? undefined : redactRuntimeText(value, max);
|
|
462
|
+
const projectSkill = (skill) => ({
|
|
463
|
+
name: field(skill.name, 200) ?? "",
|
|
464
|
+
description: field(skill.description, 600) ?? "",
|
|
465
|
+
path: field(skill.path, 400) ?? "",
|
|
466
|
+
...(skill.baseDir ? { baseDir: field(skill.baseDir, 400) } : {}),
|
|
467
|
+
scope: skill.scope,
|
|
468
|
+
source: field(skill.source, 200) ?? "",
|
|
469
|
+
enabled: skill.enabled,
|
|
470
|
+
diagnostics: skill.diagnostics.map((diag) => field(diag, 400) ?? ""),
|
|
471
|
+
});
|
|
472
|
+
const projectExtension = (ext) => ({
|
|
473
|
+
name: field(ext.name, 200) ?? "",
|
|
474
|
+
path: field(ext.path, 400) ?? "",
|
|
475
|
+
...(ext.resolvedPath
|
|
476
|
+
? { resolvedPath: field(ext.resolvedPath, 400) }
|
|
477
|
+
: {}),
|
|
478
|
+
source: field(ext.source, 200) ?? "",
|
|
479
|
+
scope: ext.scope,
|
|
480
|
+
...(ext.origin ? { origin: field(ext.origin, 200) } : {}),
|
|
481
|
+
enabled: ext.enabled,
|
|
482
|
+
...(ext.error ? { error: field(ext.error, 400) } : {}),
|
|
483
|
+
});
|
|
484
|
+
const projectPackage = (pkg) => ({
|
|
485
|
+
spec: field(pkg.spec, 200) ?? "",
|
|
486
|
+
scope: pkg.scope,
|
|
487
|
+
enabled: pkg.enabled,
|
|
488
|
+
...(pkg.version ? { version: field(pkg.version, 80) } : {}),
|
|
489
|
+
...(pkg.configuredVersion
|
|
490
|
+
? { configuredVersion: field(pkg.configuredVersion, 80) }
|
|
491
|
+
: {}),
|
|
492
|
+
...(pkg.packageName ? { packageName: field(pkg.packageName, 200) } : {}),
|
|
493
|
+
...(pkg.path ? { path: field(pkg.path, 400) } : {}),
|
|
494
|
+
...(pkg.installedPath ? { installedPath: field(pkg.installedPath, 400) } : {}),
|
|
495
|
+
...(pkg.cwd ? { cwd: field(pkg.cwd, 400) } : {}),
|
|
496
|
+
resolved: pkg.resolved.map((resource) => ({
|
|
497
|
+
type: resource.type,
|
|
498
|
+
name: field(resource.name, 200) ?? "",
|
|
499
|
+
path: field(resource.path, 400) ?? "",
|
|
500
|
+
source: field(resource.source, 200) ?? "",
|
|
501
|
+
scope: resource.scope,
|
|
502
|
+
})),
|
|
503
|
+
diagnostics: pkg.diagnostics.map((diag) => field(diag, 400) ?? ""),
|
|
504
|
+
});
|
|
505
|
+
const projectDiagnostic = (diag) => ({
|
|
506
|
+
type: diag.type,
|
|
507
|
+
...(diag.code ? { code: diag.code } : {}),
|
|
508
|
+
message: field(diag.message, 400) ?? "",
|
|
509
|
+
...(diag.path ? { path: field(diag.path, 400) } : {}),
|
|
510
|
+
...(diag.retryable !== undefined
|
|
511
|
+
? { retryable: diag.retryable }
|
|
512
|
+
: {}),
|
|
513
|
+
});
|
|
514
|
+
return {
|
|
515
|
+
sessionId: input.sessionId,
|
|
516
|
+
revision: input.revision,
|
|
517
|
+
loadedAt: input.loadedAt,
|
|
518
|
+
selection: {
|
|
519
|
+
...(input.selection.model
|
|
520
|
+
? {
|
|
521
|
+
model: {
|
|
522
|
+
provider: input.selection.model.provider,
|
|
523
|
+
id: input.selection.model.id,
|
|
524
|
+
...(input.selection.model.name
|
|
525
|
+
? { name: field(input.selection.model.name, 200) }
|
|
526
|
+
: {}),
|
|
527
|
+
},
|
|
528
|
+
}
|
|
529
|
+
: {}),
|
|
530
|
+
thinkingSelection: input.selection.thinkingSelection,
|
|
531
|
+
...(input.selection.effectiveThinkingLevel
|
|
532
|
+
? { effectiveThinkingLevel: input.selection.effectiveThinkingLevel }
|
|
533
|
+
: {}),
|
|
534
|
+
},
|
|
535
|
+
models: input.models.map((model) => ({
|
|
536
|
+
provider: model.provider,
|
|
537
|
+
id: model.id,
|
|
538
|
+
...(model.name ? { name: field(model.name, 200) } : {}),
|
|
539
|
+
...(model.hasCredentials !== undefined
|
|
540
|
+
? { hasCredentials: model.hasCredentials }
|
|
541
|
+
: {}),
|
|
542
|
+
})),
|
|
543
|
+
thinkingLevels: input.thinkingLevels.map((option) => ({ ...option })),
|
|
544
|
+
systemPrompt: {
|
|
545
|
+
...(input.systemPrompt.raw !== undefined
|
|
546
|
+
? { raw: input.systemPrompt.raw }
|
|
547
|
+
: {}),
|
|
548
|
+
text: redactRuntimeText(input.systemPrompt.text, RUNTIME_CONTEXT_TEXT_MAX),
|
|
549
|
+
sha256: input.systemPrompt.sha256,
|
|
550
|
+
characters: input.systemPrompt.characters,
|
|
551
|
+
},
|
|
552
|
+
activeTools: [...input.activeTools],
|
|
553
|
+
contextFiles: input.contextFiles.map((file) => ({
|
|
554
|
+
path: field(file.path, 400) ?? "",
|
|
555
|
+
characters: file.characters,
|
|
556
|
+
})),
|
|
557
|
+
skills: input.skills.map(projectSkill),
|
|
558
|
+
extensions: input.extensions.map(projectExtension),
|
|
559
|
+
packages: input.packages.map(projectPackage),
|
|
560
|
+
diagnostics: input.diagnostics.map(projectDiagnostic),
|
|
561
|
+
hasBash: input.hasBash,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
385
564
|
/**
|
|
386
565
|
* Extract the latest assistant text fragment from an SDK message object.
|
|
387
566
|
* Tolerant of multiple message shapes the SDK emits.
|
|
@@ -433,9 +612,16 @@ export class ConsolePiRuntime {
|
|
|
433
612
|
sessions = new Map();
|
|
434
613
|
modelRuntimes = new Map();
|
|
435
614
|
sessionManagers = new Map();
|
|
615
|
+
/** Per-session SDK service scope (resourceLoader / settingsManager) used by the live runtime snapshot. */
|
|
616
|
+
serviceScopes = new Map();
|
|
617
|
+
/** Snapshot revision: bumped on create / reopen / model / thinking / reload. */
|
|
618
|
+
revisions = new Map();
|
|
619
|
+
/** RF-01: per-session reload in-flight lock (concurrent second call → PI_SESSION_BUSY). */
|
|
620
|
+
reloadInFlight = new Set();
|
|
621
|
+
/** Composer thinking selection ("auto" = no explicit override). */
|
|
622
|
+
thinkingSelections = new Map();
|
|
436
623
|
mainlineLeaves = new Map();
|
|
437
624
|
bindings;
|
|
438
|
-
disabledInstructionSkills = new Set();
|
|
439
625
|
constructor(options) {
|
|
440
626
|
this.options = options;
|
|
441
627
|
this.loader = createOperatorChatResourceLoader();
|
|
@@ -461,9 +647,6 @@ export class ConsolePiRuntime {
|
|
|
461
647
|
})),
|
|
462
648
|
};
|
|
463
649
|
}
|
|
464
|
-
setDisabledInstructionSkills(names) {
|
|
465
|
-
this.disabledInstructionSkills = new Set(names);
|
|
466
|
-
}
|
|
467
650
|
/** Operator action context used by custom-tool dispatchers (Gate 3). */
|
|
468
651
|
get actionContext() {
|
|
469
652
|
return this.options.actionContext;
|
|
@@ -497,22 +680,6 @@ export class ConsolePiRuntime {
|
|
|
497
680
|
modelId: descriptor.modelId,
|
|
498
681
|
};
|
|
499
682
|
}
|
|
500
|
-
/**
|
|
501
|
-
* Load the operator-context instruction skills (plan D1) and compose the
|
|
502
|
-
* read-only methodology fragment for the Chat system prompt. Returns an
|
|
503
|
-
* empty string when no skills load (e.g. skills dir missing) so the
|
|
504
|
-
* session still boots — skills are context, not a hard dependency.
|
|
505
|
-
*/
|
|
506
|
-
async composeSystemPromptSkills() {
|
|
507
|
-
try {
|
|
508
|
-
const result = await loadOperatorChatInstructionSkills(this.options.skillsDir, OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS.filter((name) => !this.disabledInstructionSkills.has(name)));
|
|
509
|
-
return composeInstructionSkillsPrompt(result.loaded);
|
|
510
|
-
}
|
|
511
|
-
catch (error) {
|
|
512
|
-
process.stderr.write(`[console] chat instruction-skills load failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
513
|
-
return "";
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
683
|
/** Create a new isolated Chat session pinned to the operator-chat tool set. */
|
|
517
684
|
async createSession(init) {
|
|
518
685
|
const sessionId = `operator-chat-${randomBytes(12).toString("hex")}`;
|
|
@@ -528,28 +695,18 @@ export class ConsolePiRuntime {
|
|
|
528
695
|
}
|
|
529
696
|
// Final fallback only when nothing resolved: let the SDK pick its default
|
|
530
697
|
// (we do NOT hardcode a provider here).
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
|
|
698
|
+
// Standard open resource surface (ADR 0012): the SDK DefaultResourceLoader
|
|
699
|
+
// loads user/project AGENTS.md context files, skills (~/.pi/agent/skills/
|
|
700
|
+
// + .agents/skills/ init projection) and user/project extensions. Only
|
|
701
|
+
// appendSystemPrompt carries the operator-chat system prompt.
|
|
535
702
|
const appendSystemPrompt = [
|
|
536
703
|
OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
537
|
-
...(skillFragment ? [skillFragment] : []),
|
|
538
704
|
...(init?.systemPromptSuffix ? [init.systemPromptSuffix] : []),
|
|
539
705
|
];
|
|
540
706
|
const { services } = await this.bindings.createServices({
|
|
541
707
|
cwd: this.options.cwd,
|
|
542
708
|
agentDir,
|
|
543
709
|
resourceLoaderOptions: {
|
|
544
|
-
// Closed surface: do not load user/project extensions, skills,
|
|
545
|
-
// AGENTS.md, context files. The Chat tool surface is fixed by
|
|
546
|
-
// the whitelist, not by filesystem discovery.
|
|
547
|
-
noContextFiles: true,
|
|
548
|
-
noSkills: true,
|
|
549
|
-
noExtensions: true,
|
|
550
|
-
// Inject the audited instruction skills as READ-ONLY context
|
|
551
|
-
// (plan D1). appendSystemPrompt is the SDK's documented entry
|
|
552
|
-
// point for adding system-prompt text without enabling tools.
|
|
553
710
|
appendSystemPrompt,
|
|
554
711
|
},
|
|
555
712
|
});
|
|
@@ -633,12 +790,19 @@ export class ConsolePiRuntime {
|
|
|
633
790
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
634
791
|
});
|
|
635
792
|
// Gate 2: re-pin active tools (defensive — some SDK paths may seed
|
|
636
|
-
// defaults). Active set =
|
|
637
|
-
//
|
|
638
|
-
|
|
793
|
+
// defaults). Active set = operator baseline + built-in explore tools +
|
|
794
|
+
// extension tools registered by user/project extensions (ADR 0012).
|
|
795
|
+
// Non-Pi write channels are excluded above and cannot appear here.
|
|
796
|
+
session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
|
|
639
797
|
this.sessions.set(sessionId, session);
|
|
640
798
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
641
799
|
this.modelRuntimes.set(sessionId, modelRuntime);
|
|
800
|
+
this.serviceScopes.set(sessionId, {
|
|
801
|
+
resourceLoader: services.resourceLoader,
|
|
802
|
+
settingsManager: services.settingsManager,
|
|
803
|
+
});
|
|
804
|
+
this.revisions.set(sessionId, 1);
|
|
805
|
+
this.thinkingSelections.set(sessionId, "auto");
|
|
642
806
|
return {
|
|
643
807
|
sessionId,
|
|
644
808
|
sessionFile: session.sessionFile,
|
|
@@ -661,10 +825,13 @@ export class ConsolePiRuntime {
|
|
|
661
825
|
sessionManager,
|
|
662
826
|
model: init.model,
|
|
663
827
|
});
|
|
664
|
-
built.session.setActiveToolsByName(
|
|
828
|
+
built.session.setActiveToolsByName(computeOperatorChatActiveToolNames(built.session));
|
|
665
829
|
this.sessions.set(sessionId, built.session);
|
|
666
830
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
667
831
|
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
832
|
+
this.serviceScopes.set(sessionId, built.services);
|
|
833
|
+
this.revisions.set(sessionId, 1);
|
|
834
|
+
this.thinkingSelections.set(sessionId, "auto");
|
|
668
835
|
return {
|
|
669
836
|
sessionId,
|
|
670
837
|
sessionFile: built.session.sessionFile,
|
|
@@ -729,18 +896,31 @@ export class ConsolePiRuntime {
|
|
|
729
896
|
if (!resolved)
|
|
730
897
|
throw new Error("MODEL_NOT_AVAILABLE");
|
|
731
898
|
await session.setModel(resolved);
|
|
899
|
+
this.bumpRevision(sessionId);
|
|
732
900
|
return {
|
|
733
901
|
provider: session.model?.provider ?? model.provider,
|
|
734
902
|
modelId: session.model?.id ?? session.model?.modelId ?? model.modelId,
|
|
735
903
|
};
|
|
736
904
|
}
|
|
905
|
+
/**
|
|
906
|
+
* Apply a thinking-level selection. `auto` records the selection WITHOUT
|
|
907
|
+
* sending any explicit override (the SDK keeps its own default); an explicit
|
|
908
|
+
* level calls `session.setThinkingLevel`. Returns the effective value.
|
|
909
|
+
*/
|
|
737
910
|
applyThinkingLevel(sessionId, level) {
|
|
738
911
|
const session = this.sessions.get(sessionId);
|
|
739
912
|
if (!session)
|
|
740
913
|
throw new Error(`chat session not active: ${sessionId}`);
|
|
914
|
+
if (level === "auto") {
|
|
915
|
+
this.thinkingSelections.set(sessionId, "auto");
|
|
916
|
+
this.bumpRevision(sessionId);
|
|
917
|
+
return "auto";
|
|
918
|
+
}
|
|
741
919
|
if (!THINKING_LEVELS.includes(level) || !session.setThinkingLevel)
|
|
742
920
|
throw new Error("THINKING_LEVEL_UNSUPPORTED");
|
|
743
921
|
session.setThinkingLevel(level);
|
|
922
|
+
this.thinkingSelections.set(sessionId, level);
|
|
923
|
+
this.bumpRevision(sessionId);
|
|
744
924
|
return session.thinkingLevel ?? level;
|
|
745
925
|
}
|
|
746
926
|
getRuntimeSelection(sessionId) {
|
|
@@ -757,6 +937,370 @@ export class ConsolePiRuntime {
|
|
|
757
937
|
activeTools: session.getActiveToolNames(),
|
|
758
938
|
};
|
|
759
939
|
}
|
|
940
|
+
bumpRevision(sessionId) {
|
|
941
|
+
this.revisions.set(sessionId, (this.revisions.get(sessionId) ?? 0) + 1);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Resolve the RAW system prompt actually injected into the Pi session
|
|
945
|
+
* (design §2.2 / AC-03). Fallback chain: agent.state.systemPrompt →
|
|
946
|
+
* session.systemPrompt getter → ResourceLoader.getSystemPrompt() → the
|
|
947
|
+
* composed operator-chat base prompt. Never returns empty.
|
|
948
|
+
*/
|
|
949
|
+
async getRuntimeSystemPrompt(sessionId) {
|
|
950
|
+
const session = this.sessions.get(sessionId);
|
|
951
|
+
if (!session)
|
|
952
|
+
return composeOperatorChatSystemPrompt({});
|
|
953
|
+
const fromState = session.state?.systemPrompt;
|
|
954
|
+
const fromGetter = session.systemPrompt;
|
|
955
|
+
const loader = this.serviceScopes.get(sessionId)?.resourceLoader;
|
|
956
|
+
return (fromState ||
|
|
957
|
+
fromGetter ||
|
|
958
|
+
loader?.getSystemPrompt?.() ||
|
|
959
|
+
composeOperatorChatSystemPrompt({}));
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Read the RAW system prompt for the snapshot, converting any projection
|
|
963
|
+
* failure into the structured critical error (RF-02): a broken system prompt
|
|
964
|
+
* source must fail the whole snapshot with 503 PI_RUNTIME_UNAVAILABLE rather
|
|
965
|
+
* than silently fabricate prompt text.
|
|
966
|
+
*/
|
|
967
|
+
async readRawSystemPrompt(sessionId) {
|
|
968
|
+
try {
|
|
969
|
+
return await this.getRuntimeSystemPrompt(sessionId);
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
throw new RuntimeSnapshotUnavailableError(`system prompt projection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Official Pi session reload (AC-05 / AC-06). Rejects with a structured
|
|
977
|
+
* code while the session is busy (streaming / compacting / bash in flight /
|
|
978
|
+
* duplicate reload) — never aborts, never silently queues.
|
|
979
|
+
*/
|
|
980
|
+
async reloadSession(sessionId) {
|
|
981
|
+
const session = this.sessions.get(sessionId);
|
|
982
|
+
if (!session) {
|
|
983
|
+
return {
|
|
984
|
+
ok: false,
|
|
985
|
+
code: "PI_SESSION_NOT_FOUND",
|
|
986
|
+
message: `chat session not active: ${sessionId}`,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
// RF2-03: the per-sessionId in-flight lock is checked FIRST — before the
|
|
990
|
+
// streaming/compacting/bash/support checks. JS runs check-then-add in one
|
|
991
|
+
// synchronous segment, so two concurrent calls cannot both pass; the lock
|
|
992
|
+
// is released in `finally` on BOTH the success and the error path, and
|
|
993
|
+
// different sessions never block each other. Priority matters: the first
|
|
994
|
+
// reload() may synchronously flip isIdle/isCompacting/isBashRunning/
|
|
995
|
+
// hasPendingBashMessages to busy, and a second concurrent call must still
|
|
996
|
+
// report the reload-in-flight busy (not the streaming/compacting one).
|
|
997
|
+
if (this.reloadInFlight.has(sessionId)) {
|
|
998
|
+
return {
|
|
999
|
+
ok: false,
|
|
1000
|
+
code: "PI_SESSION_BUSY",
|
|
1001
|
+
message: "session is busy (reload in flight); reload rejected without abort",
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
const busyReasons = [];
|
|
1005
|
+
if (session.isStreaming === true || session.isIdle === false)
|
|
1006
|
+
busyReasons.push("streaming");
|
|
1007
|
+
if (session.isCompacting === true)
|
|
1008
|
+
busyReasons.push("compacting");
|
|
1009
|
+
if (session.isBashRunning === true)
|
|
1010
|
+
busyReasons.push("bash running");
|
|
1011
|
+
if (session.hasPendingBashMessages === true)
|
|
1012
|
+
busyReasons.push("bash messages pending");
|
|
1013
|
+
if (busyReasons.length > 0) {
|
|
1014
|
+
return {
|
|
1015
|
+
ok: false,
|
|
1016
|
+
code: "PI_SESSION_BUSY",
|
|
1017
|
+
message: `session is busy (${busyReasons.join(", ")}); reload rejected without abort`,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
if (typeof session.reload !== "function") {
|
|
1021
|
+
return {
|
|
1022
|
+
ok: false,
|
|
1023
|
+
code: "PI_RELOAD_FAILED",
|
|
1024
|
+
message: "this Pi runtime does not support session.reload()",
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
this.reloadInFlight.add(sessionId);
|
|
1028
|
+
try {
|
|
1029
|
+
await session.reload();
|
|
1030
|
+
}
|
|
1031
|
+
catch (error) {
|
|
1032
|
+
return {
|
|
1033
|
+
ok: false,
|
|
1034
|
+
code: "PI_RELOAD_FAILED",
|
|
1035
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
finally {
|
|
1039
|
+
this.reloadInFlight.delete(sessionId);
|
|
1040
|
+
}
|
|
1041
|
+
this.bumpRevision(sessionId);
|
|
1042
|
+
return { ok: true, revision: this.revisions.get(sessionId) ?? 0 };
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Project the unified live runtime snapshot (design §2.2). The current Pi
|
|
1046
|
+
* session is the single source of truth: real system prompt, real model /
|
|
1047
|
+
* thinking selection (auto + effective), the session modelRuntime model
|
|
1048
|
+
* list, and the session ResourceLoader inventories. Undefined when the
|
|
1049
|
+
* session is not active — callers fail closed instead of fabricating data.
|
|
1050
|
+
*/
|
|
1051
|
+
async getRuntimeSnapshot(sessionId, options) {
|
|
1052
|
+
const session = this.sessions.get(sessionId);
|
|
1053
|
+
if (!session)
|
|
1054
|
+
return undefined;
|
|
1055
|
+
const services = this.serviceScopes.get(sessionId);
|
|
1056
|
+
const modelRuntime = this.modelRuntimes.get(sessionId);
|
|
1057
|
+
const resourceLoader = services?.resourceLoader;
|
|
1058
|
+
const settingsManager = services?.settingsManager;
|
|
1059
|
+
const thinkingSelection = this.thinkingSelections.get(sessionId) ?? "auto";
|
|
1060
|
+
const effectiveThinkingLevel = session.thinkingLevel ??
|
|
1061
|
+
settingsManager?.getDefaultThinkingLevel?.() ??
|
|
1062
|
+
(thinkingSelection === "auto" ? undefined : thinkingSelection);
|
|
1063
|
+
const rawSystemPrompt = await this.readRawSystemPrompt(sessionId);
|
|
1064
|
+
// Models come from the session modelRuntime. A model-enumeration failure
|
|
1065
|
+
// is a CRITICAL projection failure (RF-02): it throws
|
|
1066
|
+
// RuntimeSnapshotUnavailableError so consuming routes return 503
|
|
1067
|
+
// PI_RUNTIME_UNAVAILABLE — never a fabricated empty model list.
|
|
1068
|
+
let models = [];
|
|
1069
|
+
if (modelRuntime) {
|
|
1070
|
+
try {
|
|
1071
|
+
models = this.bindings
|
|
1072
|
+
.listAvailableModels({ modelRuntime })
|
|
1073
|
+
.map((entry) => ({
|
|
1074
|
+
provider: entry.provider,
|
|
1075
|
+
id: entry.id,
|
|
1076
|
+
...(entry.name ? { name: entry.name } : {}),
|
|
1077
|
+
hasCredentials: entry.hasCredentials,
|
|
1078
|
+
}));
|
|
1079
|
+
}
|
|
1080
|
+
catch (error) {
|
|
1081
|
+
throw new RuntimeSnapshotUnavailableError(`model enumeration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
// Thinking options: auto is always offered; explicit levels only when the
|
|
1085
|
+
// runtime session supports setThinkingLevel (xhigh/max ship with the SDK
|
|
1086
|
+
// and therefore surface only when this runtime provides them).
|
|
1087
|
+
const supportedLevels = typeof session.setThinkingLevel === "function"
|
|
1088
|
+
? [...THINKING_LEVELS]
|
|
1089
|
+
: [];
|
|
1090
|
+
const thinkingLevels = [
|
|
1091
|
+
{ value: "auto" },
|
|
1092
|
+
...supportedLevels.map((value) => ({ value })),
|
|
1093
|
+
];
|
|
1094
|
+
// Resource inventories from the session ResourceLoader (ADR 0012 open
|
|
1095
|
+
// surface). Missing loader → real empty lists, never fabricated entries.
|
|
1096
|
+
// Each subface is isolated (RF-02): a throwing subface keeps the other
|
|
1097
|
+
// inventories AND surfaces a structured retryable diagnostic instead of
|
|
1098
|
+
// silently dropping the whole resource projection.
|
|
1099
|
+
const contextFiles = [];
|
|
1100
|
+
const skills = [];
|
|
1101
|
+
const extensions = [];
|
|
1102
|
+
const diagnostics = [];
|
|
1103
|
+
let resolvedResources = [];
|
|
1104
|
+
const resourceFailure = (subface, error) => {
|
|
1105
|
+
diagnostics.push({
|
|
1106
|
+
type: "error",
|
|
1107
|
+
code: "RESOURCE_LOAD_FAILED",
|
|
1108
|
+
message: `${subface} inventory failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1109
|
+
retryable: true,
|
|
1110
|
+
});
|
|
1111
|
+
};
|
|
1112
|
+
if (resourceLoader) {
|
|
1113
|
+
try {
|
|
1114
|
+
const agentsFiles = resourceLoader.getAgentsFiles?.();
|
|
1115
|
+
for (const file of agentsFiles?.agentsFiles ?? []) {
|
|
1116
|
+
if (!file.path)
|
|
1117
|
+
continue;
|
|
1118
|
+
contextFiles.push({
|
|
1119
|
+
path: file.path,
|
|
1120
|
+
characters: file.content?.length ?? 0,
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
catch (error) {
|
|
1125
|
+
resourceFailure("context files", error);
|
|
1126
|
+
}
|
|
1127
|
+
try {
|
|
1128
|
+
const skillsResult = resourceLoader.getSkills?.();
|
|
1129
|
+
for (const skill of skillsResult?.skills ?? []) {
|
|
1130
|
+
skills.push({
|
|
1131
|
+
name: skill.name,
|
|
1132
|
+
description: skill.description ?? "",
|
|
1133
|
+
path: skill.filePath,
|
|
1134
|
+
...(skill.baseDir ? { baseDir: skill.baseDir } : {}),
|
|
1135
|
+
scope: skill.sourceInfo?.scope ?? "unknown",
|
|
1136
|
+
source: skill.sourceInfo?.source ?? "",
|
|
1137
|
+
enabled: !skill.disableModelInvocation,
|
|
1138
|
+
diagnostics: [],
|
|
1139
|
+
});
|
|
1140
|
+
resolvedResources.push({
|
|
1141
|
+
type: "skill",
|
|
1142
|
+
name: skill.name,
|
|
1143
|
+
path: skill.filePath,
|
|
1144
|
+
source: skill.sourceInfo?.source ?? "",
|
|
1145
|
+
scope: skill.sourceInfo?.scope ?? "unknown",
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
for (const diag of skillsResult?.diagnostics ?? []) {
|
|
1149
|
+
if (!diag?.message)
|
|
1150
|
+
continue;
|
|
1151
|
+
diagnostics.push({
|
|
1152
|
+
type: diag.type ?? "warning",
|
|
1153
|
+
message: diag.message,
|
|
1154
|
+
...(diag.path ? { path: diag.path } : {}),
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
catch (error) {
|
|
1159
|
+
resourceFailure("skills", error);
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
const extResult = resourceLoader.getExtensions?.();
|
|
1163
|
+
for (const ext of extResult?.extensions ?? []) {
|
|
1164
|
+
const path = ext.resolvedPath ?? ext.path ?? "";
|
|
1165
|
+
extensions.push({
|
|
1166
|
+
name: ext.sourceInfo?.source ?? ext.path ?? "extension",
|
|
1167
|
+
path,
|
|
1168
|
+
...(ext.resolvedPath ? { resolvedPath: ext.resolvedPath } : {}),
|
|
1169
|
+
source: ext.sourceInfo?.source ?? "",
|
|
1170
|
+
scope: ext.sourceInfo?.scope ?? "unknown",
|
|
1171
|
+
...(ext.sourceInfo?.origin ? { origin: ext.sourceInfo.origin } : {}),
|
|
1172
|
+
enabled: true,
|
|
1173
|
+
});
|
|
1174
|
+
resolvedResources.push({
|
|
1175
|
+
type: "extension",
|
|
1176
|
+
name: ext.sourceInfo?.source ?? ext.path ?? "extension",
|
|
1177
|
+
path,
|
|
1178
|
+
source: ext.sourceInfo?.source ?? "",
|
|
1179
|
+
scope: ext.sourceInfo?.scope ?? "unknown",
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
for (const err of extResult?.errors ?? []) {
|
|
1183
|
+
if (!err?.error)
|
|
1184
|
+
continue;
|
|
1185
|
+
diagnostics.push({
|
|
1186
|
+
type: "error",
|
|
1187
|
+
message: err.error,
|
|
1188
|
+
...(err.path ? { path: err.path } : {}),
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
catch (error) {
|
|
1193
|
+
resourceFailure("extensions", error);
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
const promptsResult = resourceLoader.getPrompts?.();
|
|
1197
|
+
for (const prompt of promptsResult?.prompts ?? []) {
|
|
1198
|
+
resolvedResources.push({
|
|
1199
|
+
type: "prompt",
|
|
1200
|
+
name: prompt.name,
|
|
1201
|
+
path: prompt.filePath,
|
|
1202
|
+
source: prompt.sourceInfo?.source ?? "",
|
|
1203
|
+
scope: prompt.sourceInfo?.scope ?? "unknown",
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
for (const diag of promptsResult?.diagnostics ?? []) {
|
|
1207
|
+
if (!diag?.message)
|
|
1208
|
+
continue;
|
|
1209
|
+
diagnostics.push({
|
|
1210
|
+
type: diag.type ?? "warning",
|
|
1211
|
+
message: diag.message,
|
|
1212
|
+
...(diag.path ? { path: diag.path } : {}),
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
resourceFailure("prompts", error);
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
const themesResult = resourceLoader.getThemes?.();
|
|
1221
|
+
for (const theme of themesResult?.themes ?? []) {
|
|
1222
|
+
if (!theme.path)
|
|
1223
|
+
continue;
|
|
1224
|
+
resolvedResources.push({
|
|
1225
|
+
type: "theme",
|
|
1226
|
+
name: theme.name ?? theme.path,
|
|
1227
|
+
path: theme.path,
|
|
1228
|
+
source: "",
|
|
1229
|
+
scope: "unknown",
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
for (const diag of themesResult?.diagnostics ?? []) {
|
|
1233
|
+
if (!diag?.message)
|
|
1234
|
+
continue;
|
|
1235
|
+
diagnostics.push({
|
|
1236
|
+
type: diag.type ?? "warning",
|
|
1237
|
+
message: diag.message,
|
|
1238
|
+
...(diag.path ? { path: diag.path } : {}),
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
catch (error) {
|
|
1243
|
+
resourceFailure("themes", error);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
// Packages: Pi settings specs + resolved resource metadata. Fields the SDK
|
|
1247
|
+
// does not expose stay absent (real missing, never fabricated). A
|
|
1248
|
+
// readPackageInventory failure is a PARTIAL failure (RF-02): the snapshot
|
|
1249
|
+
// stays 200 with a structured retryable diagnostic — never a silent
|
|
1250
|
+
// fake-empty package list.
|
|
1251
|
+
const { readPackageInventory } = await import("./pi-console-config.js");
|
|
1252
|
+
let packages = [];
|
|
1253
|
+
try {
|
|
1254
|
+
packages = (await readPackageInventory(await safeGetAgentDir(this.bindings), this.options.cwd, resolvedResources));
|
|
1255
|
+
}
|
|
1256
|
+
catch (error) {
|
|
1257
|
+
packages = [];
|
|
1258
|
+
diagnostics.push({
|
|
1259
|
+
type: "error",
|
|
1260
|
+
code: "PACKAGE_INVENTORY_FAILED",
|
|
1261
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1262
|
+
retryable: true,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
return projectRuntimeSnapshot({
|
|
1266
|
+
sessionId,
|
|
1267
|
+
revision: this.revisions.get(sessionId) ?? 0,
|
|
1268
|
+
loadedAt: new Date().toISOString(),
|
|
1269
|
+
selection: {
|
|
1270
|
+
...(session.model?.provider &&
|
|
1271
|
+
(session.model?.id ?? session.model?.modelId)
|
|
1272
|
+
? {
|
|
1273
|
+
model: {
|
|
1274
|
+
provider: session.model.provider,
|
|
1275
|
+
id: session.model.id ?? session.model.modelId,
|
|
1276
|
+
...(session.model.name ? { name: session.model.name } : {}),
|
|
1277
|
+
},
|
|
1278
|
+
}
|
|
1279
|
+
: {}),
|
|
1280
|
+
thinkingSelection,
|
|
1281
|
+
...(effectiveThinkingLevel
|
|
1282
|
+
? { effectiveThinkingLevel: effectiveThinkingLevel }
|
|
1283
|
+
: {}),
|
|
1284
|
+
},
|
|
1285
|
+
models,
|
|
1286
|
+
thinkingLevels,
|
|
1287
|
+
systemPrompt: {
|
|
1288
|
+
...(options?.includeRaw ? { raw: rawSystemPrompt } : {}),
|
|
1289
|
+
// The RAW prompt flows into the projection: text is scrubbed +
|
|
1290
|
+
// length-limited while sha256/characters stay RAW-based (design §2.2).
|
|
1291
|
+
text: rawSystemPrompt,
|
|
1292
|
+
sha256: createHash("sha256").update(rawSystemPrompt).digest("hex"),
|
|
1293
|
+
characters: rawSystemPrompt.length,
|
|
1294
|
+
},
|
|
1295
|
+
activeTools: session.getActiveToolNames(),
|
|
1296
|
+
contextFiles,
|
|
1297
|
+
skills,
|
|
1298
|
+
extensions,
|
|
1299
|
+
packages,
|
|
1300
|
+
diagnostics,
|
|
1301
|
+
hasBash: true,
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
760
1304
|
/**
|
|
761
1305
|
* Re-open a previously persisted Chat session after Console restart or
|
|
762
1306
|
* browser refresh (roadmap M1-S04 / S05).
|
|
@@ -838,10 +1382,13 @@ export class ConsolePiRuntime {
|
|
|
838
1382
|
message: `reopened Pi sessionId mismatch: expected ${sessionId}, got ${reportedId}`,
|
|
839
1383
|
};
|
|
840
1384
|
}
|
|
841
|
-
session.setActiveToolsByName(
|
|
1385
|
+
session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
|
|
842
1386
|
this.sessions.set(sessionId, session);
|
|
843
1387
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
844
1388
|
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
1389
|
+
this.serviceScopes.set(sessionId, built.services);
|
|
1390
|
+
this.revisions.set(sessionId, 1);
|
|
1391
|
+
this.thinkingSelections.set(sessionId, "auto");
|
|
845
1392
|
return {
|
|
846
1393
|
ok: true,
|
|
847
1394
|
handle: {
|
|
@@ -880,19 +1427,14 @@ export class ConsolePiRuntime {
|
|
|
880
1427
|
*/
|
|
881
1428
|
async buildSessionWithServices(init) {
|
|
882
1429
|
const agentDir = await safeGetAgentDir(this.bindings);
|
|
883
|
-
const skillFragment = await this.composeSystemPromptSkills();
|
|
884
1430
|
const appendSystemPrompt = [
|
|
885
1431
|
OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
886
|
-
...(skillFragment ? [skillFragment] : []),
|
|
887
1432
|
...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
|
|
888
1433
|
];
|
|
889
1434
|
const { services } = await this.bindings.createServices({
|
|
890
1435
|
cwd: this.options.cwd,
|
|
891
1436
|
agentDir,
|
|
892
1437
|
resourceLoaderOptions: {
|
|
893
|
-
noContextFiles: true,
|
|
894
|
-
noSkills: true,
|
|
895
|
-
noExtensions: true,
|
|
896
1438
|
appendSystemPrompt,
|
|
897
1439
|
},
|
|
898
1440
|
});
|
|
@@ -942,7 +1484,14 @@ export class ConsolePiRuntime {
|
|
|
942
1484
|
...(customTools && customTools.length > 0 ? { customTools } : {}),
|
|
943
1485
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
944
1486
|
});
|
|
945
|
-
return {
|
|
1487
|
+
return {
|
|
1488
|
+
session,
|
|
1489
|
+
modelRuntime,
|
|
1490
|
+
services: {
|
|
1491
|
+
resourceLoader: services.resourceLoader,
|
|
1492
|
+
settingsManager: services.settingsManager,
|
|
1493
|
+
},
|
|
1494
|
+
};
|
|
946
1495
|
}
|
|
947
1496
|
/** Re-open a previously created session (cross-request). */
|
|
948
1497
|
open(sessionId) {
|
|
@@ -963,11 +1512,12 @@ export class ConsolePiRuntime {
|
|
|
963
1512
|
}
|
|
964
1513
|
// Gate 2 (re-pin before each turn): interview turns pass through the
|
|
965
1514
|
// existing closed interview allow/deny registry. The next ordinary turn
|
|
966
|
-
// explicitly restores the normal Operator Chat set
|
|
1515
|
+
// explicitly restores the normal Operator Chat set (baseline + extension
|
|
1516
|
+
// tools, ADR 0012).
|
|
967
1517
|
const requestedTools = OPERATOR_CHAT_ACTIVE_TOOL_NAMES();
|
|
968
1518
|
const activeTools = options?.mode === "requirement-interview"
|
|
969
1519
|
? filterActiveInterviewTools(requestedTools).allowed
|
|
970
|
-
:
|
|
1520
|
+
: computeOperatorChatActiveToolNames(session);
|
|
971
1521
|
session.setActiveToolsByName(activeTools);
|
|
972
1522
|
const unsub = session.subscribe((event) => {
|
|
973
1523
|
const mapped = mapSdkEvent(sessionId, event);
|
|
@@ -1049,6 +1599,10 @@ export class ConsolePiRuntime {
|
|
|
1049
1599
|
this.sessionManagers.delete(sessionId);
|
|
1050
1600
|
this.mainlineLeaves.delete(sessionId);
|
|
1051
1601
|
this.modelRuntimes.delete(sessionId);
|
|
1602
|
+
this.serviceScopes.delete(sessionId);
|
|
1603
|
+
this.revisions.delete(sessionId);
|
|
1604
|
+
this.thinkingSelections.delete(sessionId);
|
|
1605
|
+
this.reloadInFlight.delete(sessionId);
|
|
1052
1606
|
}
|
|
1053
1607
|
}
|
|
1054
1608
|
disposeAll() {
|