@tea-agent/loop-agent 0.43.0-next.3 → 0.43.0-next.5
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 +81 -9
- package/dist/application/task-lifecycle/advance.js +17 -4
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +244 -33
- package/dist/executors/pi-executor.js +2 -0
- package/dist/executors/pi-sdk-executor.js +75 -39
- package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime.js +131 -95
- package/dist/worker/console/chat/routes.js +175 -54
- package/dist/worker/console/chat/scm-routes.js +193 -0
- package/dist/worker/console/chat/scm-service.js +253 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
- package/dist/worker/console/chat/session-catalog.js +1 -0
- package/dist/worker/console/chat/session-mode-view.js +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -0
- package/dist/worker/console/chat/session-store.js +22 -6
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +284 -0
- package/dist/worker/console/chat/terminal-tools.js +199 -0
- package/dist/worker/console/chat/tool-preview.js +11 -0
- package/dist/worker/console/chat/tools.js +2 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-hUmM_GRf.js → abnfDiagram-N423BO3Z-BuyrlONK.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bt-xuieW.js → arc-BvCl_wpi.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DTqkL4DJ.js → architectureDiagram-T3A2C74G-BkBpBaRq.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Cy0MNnzV.js → blockDiagram-VBNYF7ZC-yve4Mcb_.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DDdlT0Pd.js → c4Diagram-5PPSVZJV-9yLGboBx.js} +1 -1
- package/dist/worker/console/static/assets/channel-BRahIhjw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-3JawwNZ6.js → chunk-2GRJ4B5K-BKP69b8r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DoTp7Cri.js → chunk-2Q5K7J3B-NWFRnXju.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CcirhZaS.js → chunk-5RXB4S5H-BiweqLEc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-VrG21gnP.js → chunk-5VM5RSS4-CP9GHmRY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DDOgIS70.js → chunk-6Q2QTUOP-BiGDkBtj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CBjmgvJ_.js → chunk-GF5L2VYU-DkJ4ZjQS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CCQSht8M.js → chunk-JWPE2WC7-B5jjn7zq.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BpGMkUUV.js → chunk-KBJHAD2P-C6KoK9O0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DNDT41QL.js → chunk-RYQCIY6F-BvNIfyn-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-euXnDFbn.js → chunk-XXDRQBXY-BZ33iX4W.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GOC80mhq.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GOC80mhq.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BJ6NAeql.js → cose-bilkent-JH36ORCC-CP8eIuZd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-F5JKIvIq.js → cynefin-VYW2F7L2-DZECjpOe.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cwmkl_Gi.js → cynefinDiagram-MW4NZA55-CvVJoFqf.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BmCZ8815.js → dagre-VZM6K2ZE-D_JTqdAn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zWA3hUnX.js → diagram-7IWD3JNH-Db7AQ0z0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BzA-888F.js → diagram-B4RE2ZJO-AZPVo5CZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-tPNheoch.js → diagram-LBJQPF4R-BgXqI3E6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cdi7Fyqp.js → diagram-Q27KOJAE-BYjFn3Ba.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-DFK9VBf1.js → diagram-UB23O5K3-B4rJgGGO.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-xCUr1kpg.js → ebnfDiagram-BXEA7PRR-C6XWZhpw.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BANQHY_A.js → erDiagram-JOGREHBK-1YdQRIQW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-VHJYEMkP.js → flowDiagram-UKHOOZJN-l3a0AwN0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiyZQ0C6.js → ganttDiagram-PKOTCBZU-CQL96F2Z.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CZtIeBBa.js → gitGraphDiagram-DS77QQ5N-BxCvafRb.js} +1 -1
- package/dist/worker/console/static/assets/index-4wCGsXuU.js +465 -0
- package/dist/worker/console/static/assets/index-CiF9WBoR.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BuSspC0X.js → infoDiagram-6WML65LV-BUipIeDO.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DRJZyPFy.js → ishikawaDiagram-WSZJBQD7-CRfomVMX.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DIfXIVuE.js → journeyDiagram-NVQOT4AX-_UFvyviX.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CT10_7Lt.js → kanban-definition-27J2QSJJ-CKIRoqKv.js} +1 -1
- package/dist/worker/console/static/assets/{linear-ufemj74H.js → linear-CjttJDCM.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DEQe8-_m.js → mermaid.core-DPvHX155.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BRLUrbrC.js → mindmap-definition-FAOFIHXS-Qmj-qwBe.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BgibKtiF.js → pegDiagram-VL7TDLO6-Auw6DRgq.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D3eyfdV9.js → pieDiagram-7S7Q4E2Y-CqMrfD4c.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DXeTk5Px.js → quadrantDiagram-CIZ2JOQS-BAddeggH.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BgXdFEe7.js → railroadDiagram-AXF67PYL-DG1JuOu2.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CseC2A7R.js → requirementDiagram-LRYGKXZP-DLYGMKlC.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D80PFzfJ.js → sankeyDiagram-W5VNT64P-g68SbLM9.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BWzHW7LS.js → sequenceDiagram-SI44F4Z6-CmszLKv8.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-xWQYTLAn.js → sizeCapture-X5ZJPWSS-D2tEKUCj.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CLxtpZeL.js → stateDiagram-OKZ733FA-Syc5E_Jf.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CwWwuPI-.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DwugtmWV.js → swimlanes-SLNWSIFB-Ibn0IRCa.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-A7fqemr6.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DHEOwHIe.js → timeline-definition-Z64GVDOM-2ZW9W3iS.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-pmbZKlBK.js → vennDiagram-T6HMQDX7-v50HsKUh.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CivHoM5q.js → wardleyDiagram-T6FBY63Y-DqCXEoDH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BfHIE7o6.js → xychartDiagram-ELKLHX3M-vIP8Vo9d.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +20 -4
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/workspace-context.js +15 -1
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-design-policy.js +2 -1
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/package.json +6 -3
- package/skills/frontend-plan/SKILL.md +7 -0
- package/dist/worker/console/static/assets/channel-BpzRnZmU.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +0 -1
- package/dist/worker/console/static/assets/index-BlN6ytLr.css +0 -1
- package/dist/worker/console/static/assets/index-CA-jfWlE.js +0 -449
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +0 -8
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console Chat session modes (workflow | pi).
|
|
3
|
+
*
|
|
4
|
+
* sessionMode is a durable session identity, independent of per-turn
|
|
5
|
+
* operator | requirement-interview and of UI layout mode. Record is the
|
|
6
|
+
* source of truth; runtime, catalog, list/detail and the browser cache
|
|
7
|
+
* all project this field.
|
|
8
|
+
*/
|
|
9
|
+
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS, OPERATOR_CHAT_PI_BUILTIN_TOOLS, authorizeOperatorChatTool, isOperatorChatToolDenied, } from "./tools.js";
|
|
10
|
+
import { OPERATOR_CHAT_AGENT_TOOL_NAME } from "./subagents/types.js";
|
|
11
|
+
import { CONSOLE_CHAT_SESSION_MODES, DEFAULT_CONSOLE_CHAT_SESSION_MODE, } from "./session-mode-view.js";
|
|
12
|
+
export { CONSOLE_CHAT_SESSION_MODES, DEFAULT_CONSOLE_CHAT_SESSION_MODE, SESSION_MODE_COPY, } from "./session-mode-view.js";
|
|
13
|
+
export const SESSION_MODE_INVALID_CODE = "INVALID_SESSION_MODE";
|
|
14
|
+
export const SESSION_MODE_UNKNOWN_CODE = "UNKNOWN_SESSION_MODE";
|
|
15
|
+
export const SESSION_MODE_ACTION_DENIED_CODE = "SESSION_MODE_ACTION_DENIED";
|
|
16
|
+
export const SESSION_MODE_IMMUTABLE_CODE = "SESSION_MODE_IMMUTABLE";
|
|
17
|
+
export class InvalidChatSessionModeError extends Error {
|
|
18
|
+
value;
|
|
19
|
+
code = SESSION_MODE_INVALID_CODE;
|
|
20
|
+
constructor(value) {
|
|
21
|
+
super(`sessionMode must be "workflow" or "pi"${value === undefined || value === null || value === ""
|
|
22
|
+
? ""
|
|
23
|
+
: `; got ${String(value)}`}`);
|
|
24
|
+
this.value = value;
|
|
25
|
+
this.name = "InvalidChatSessionModeError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class UnknownChatSessionModeError extends Error {
|
|
29
|
+
value;
|
|
30
|
+
code = SESSION_MODE_UNKNOWN_CODE;
|
|
31
|
+
constructor(value) {
|
|
32
|
+
super(`chat session record has unknown sessionMode ${JSON.stringify(value)}; refusing to run as workflow`);
|
|
33
|
+
this.value = value;
|
|
34
|
+
this.name = "UnknownChatSessionModeError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const MODE_SET = new Set(CONSOLE_CHAT_SESSION_MODES);
|
|
38
|
+
function isSessionMode(value) {
|
|
39
|
+
return typeof value === "string" && MODE_SET.has(value);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create-path parser. Missing / null / "" → workflow. Explicit illegal
|
|
43
|
+
* values fail closed and must not silently degrade.
|
|
44
|
+
*/
|
|
45
|
+
export function parseCreateSessionMode(value) {
|
|
46
|
+
if (value === undefined || value === null || value === "") {
|
|
47
|
+
return DEFAULT_CONSOLE_CHAT_SESSION_MODE;
|
|
48
|
+
}
|
|
49
|
+
if (isSessionMode(value))
|
|
50
|
+
return value;
|
|
51
|
+
throw new InvalidChatSessionModeError(value);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Restore-path parser. Missing field → workflow (stock-data rule, not a
|
|
55
|
+
* compatibility shim). Present but unknown → fail closed.
|
|
56
|
+
*/
|
|
57
|
+
export function readStoredSessionMode(value) {
|
|
58
|
+
if (value === undefined || value === null || value === "") {
|
|
59
|
+
return DEFAULT_CONSOLE_CHAT_SESSION_MODE;
|
|
60
|
+
}
|
|
61
|
+
if (isSessionMode(value))
|
|
62
|
+
return value;
|
|
63
|
+
throw new UnknownChatSessionModeError(value);
|
|
64
|
+
}
|
|
65
|
+
export function isWorkflowSessionMode(mode) {
|
|
66
|
+
return mode === "workflow";
|
|
67
|
+
}
|
|
68
|
+
export function isPiSessionMode(mode) {
|
|
69
|
+
return mode === "pi";
|
|
70
|
+
}
|
|
71
|
+
/** Shared Console foundations that both modes keep. Not a native-only prompt. */
|
|
72
|
+
export const PI_CHAT_SYSTEM_PROMPT_BASE = [
|
|
73
|
+
"You are Console Chat in Pi mode: a coding assistant with Pi tools plus Console shared foundations.",
|
|
74
|
+
"Use read/write/edit/bash/grep/find/ls to inspect and change the workspace. apply_patch, full-tools, shell and coding-chat are denied.",
|
|
75
|
+
"Agent (Explore) is a built-in read-only subagent for path-uncertain codebase search. Do not use it for a known single-file read. Prefer safe-read/safe-grep for secret-safe probes.",
|
|
76
|
+
"ask_user_question pauses for a real user answer. todo_write tracks the current work and is restored after compression; it is not a workflow auto-continue signal. The persistent terminal tool is available.",
|
|
77
|
+
"This session keeps Console compression (L0/L1/L2, sift, strict checkpoint) and existing Aimax / SDD access. Do not create tasks, run DAGs, drive Goals, or start requirement interviews from this session.",
|
|
78
|
+
"Obey repository AGENTS.md, skills and extensions when they apply. Choosing Pi mode is not extra write authorization and does not bypass Console access control or output scrubbing.",
|
|
79
|
+
].join("\n");
|
|
80
|
+
export const CHAT_SAFE_EXPLORE_TOOL_IDS = Object.freeze([
|
|
81
|
+
"safe-read",
|
|
82
|
+
"safe-grep",
|
|
83
|
+
"git-status",
|
|
84
|
+
"git-diff",
|
|
85
|
+
]);
|
|
86
|
+
export const PI_CHAT_GENERIC_HELPER_TOOLS = Object.freeze([
|
|
87
|
+
"ask_user_question",
|
|
88
|
+
"todo_write",
|
|
89
|
+
]);
|
|
90
|
+
export const PI_CHAT_WORKFLOW_TOOL_IDS = Object.freeze([
|
|
91
|
+
"get_goal",
|
|
92
|
+
"create_goal",
|
|
93
|
+
"update_goal",
|
|
94
|
+
]);
|
|
95
|
+
const CHAT_EXTENSION_DENIED_TOOL_IDS = new Set([
|
|
96
|
+
"apply_patch",
|
|
97
|
+
"apply-patch",
|
|
98
|
+
"full-tools",
|
|
99
|
+
"full_tools",
|
|
100
|
+
"coding-chat",
|
|
101
|
+
"coding_chat",
|
|
102
|
+
"shell",
|
|
103
|
+
].map((name) => name.toLowerCase()));
|
|
104
|
+
const WORKFLOW_OPERATOR_TOOL_IDS = new Set(OPERATOR_CHAT_ALLOWED_TOOLS.map((name) => name.toLowerCase()));
|
|
105
|
+
const PI_GOAL_TOOL_IDS = new Set(PI_CHAT_WORKFLOW_TOOL_IDS.map((name) => name.toLowerCase()));
|
|
106
|
+
export function workflowActiveToolBaseline() {
|
|
107
|
+
return [
|
|
108
|
+
...OPERATOR_CHAT_ALLOWED_TOOLS,
|
|
109
|
+
...OPERATOR_CHAT_PI_BUILTIN_TOOLS,
|
|
110
|
+
...CHAT_SAFE_EXPLORE_TOOL_IDS,
|
|
111
|
+
...OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS,
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
export function piActiveToolBaseline() {
|
|
115
|
+
return [
|
|
116
|
+
...OPERATOR_CHAT_PI_BUILTIN_TOOLS,
|
|
117
|
+
...CHAT_SAFE_EXPLORE_TOOL_IDS,
|
|
118
|
+
...OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS,
|
|
119
|
+
...PI_CHAT_GENERIC_HELPER_TOOLS,
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
export function chatActiveToolBaseline(sessionMode) {
|
|
123
|
+
return sessionMode === "pi"
|
|
124
|
+
? piActiveToolBaseline()
|
|
125
|
+
: workflowActiveToolBaseline();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Gate 2 pin: mode baseline plus user/project extension tools. Pi mode must
|
|
129
|
+
* not re-add operator actions or Goal tools via the extras pass.
|
|
130
|
+
*/
|
|
131
|
+
export function computeChatActiveToolNames(session, sessionMode) {
|
|
132
|
+
const baseline = chatActiveToolBaseline(sessionMode);
|
|
133
|
+
if (!session?.getAllTools)
|
|
134
|
+
return baseline;
|
|
135
|
+
const baselineLower = new Set(baseline.map((name) => name.toLowerCase()));
|
|
136
|
+
const extras = [];
|
|
137
|
+
for (const tool of session.getAllTools()) {
|
|
138
|
+
const name = tool?.name;
|
|
139
|
+
if (!name || typeof name !== "string")
|
|
140
|
+
continue;
|
|
141
|
+
const lower = name.toLowerCase();
|
|
142
|
+
if (baselineLower.has(lower))
|
|
143
|
+
continue;
|
|
144
|
+
if (CHAT_EXTENSION_DENIED_TOOL_IDS.has(lower))
|
|
145
|
+
continue;
|
|
146
|
+
if (sessionMode === "pi" && WORKFLOW_OPERATOR_TOOL_IDS.has(lower))
|
|
147
|
+
continue;
|
|
148
|
+
if (sessionMode === "pi" && PI_GOAL_TOOL_IDS.has(lower))
|
|
149
|
+
continue;
|
|
150
|
+
extras.push(name);
|
|
151
|
+
}
|
|
152
|
+
return extras.length > 0 ? [...baseline, ...extras] : baseline;
|
|
153
|
+
}
|
|
154
|
+
export function authorizeChatSessionTool(toolId, sessionMode) {
|
|
155
|
+
if (sessionMode === "workflow")
|
|
156
|
+
return authorizeOperatorChatTool(toolId);
|
|
157
|
+
const id = toolId.trim();
|
|
158
|
+
if (!id) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
code: "tool-unknown",
|
|
162
|
+
toolId: id,
|
|
163
|
+
message: "empty tool id is not allowed in Pi mode",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (isOperatorChatToolDenied(id)) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
code: "tool-denied",
|
|
170
|
+
toolId: id,
|
|
171
|
+
message: `tool "${id}" is denied in Pi mode (no apply_patch/full-tools/shell/coding-chat)`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const lower = id.toLowerCase();
|
|
175
|
+
if (WORKFLOW_OPERATOR_TOOL_IDS.has(lower) || PI_GOAL_TOOL_IDS.has(lower)) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
code: "tool-denied",
|
|
179
|
+
toolId: id,
|
|
180
|
+
message: `tool "${id}" is a workflow action and is not available in Pi mode`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const authorized = new Set(piActiveToolBaseline().map((name) => name.toLowerCase()));
|
|
184
|
+
if (authorized.has(lower))
|
|
185
|
+
return { ok: true, toolId: id };
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
code: "tool-unknown",
|
|
189
|
+
toolId: id,
|
|
190
|
+
message: `tool "${id}" is not registered for Pi mode`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export function composeChatSystemPromptParts(input) {
|
|
194
|
+
const base = input.sessionMode === "pi"
|
|
195
|
+
? PI_CHAT_SYSTEM_PROMPT_BASE
|
|
196
|
+
: input.workflowPrompt;
|
|
197
|
+
const suffix = input.sessionMode === "workflow" ? input.systemPromptSuffix?.trim() : "";
|
|
198
|
+
return suffix ? [base, suffix] : [base];
|
|
199
|
+
}
|
|
200
|
+
export function composeChatSystemPrompt(input) {
|
|
201
|
+
return composeChatSystemPromptParts(input).join("\n\n");
|
|
202
|
+
}
|
|
203
|
+
export const PI_TODO_WRITE_DESCRIPTION = "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (no partial updates, no per-item edits). Before starting multi-step work, add one todo per concrete step. Keep at least one todo `in_progress` while work remains. Mark a todo `completed` immediately when it is done (do not batch completions) and resend the full list as progress advances. Skip the list for simple Q&A, read-only lookups, and single-step tasks. Statuses: `pending` / `in_progress` / `completed`.";
|
|
204
|
+
// Slash gating lives in the browser-safe view module (shortcuts.ts is in the
|
|
205
|
+
// UI bundle); re-export here for Node-side consumers.
|
|
206
|
+
export { WORKFLOW_EXCLUSIVE_SLASH_ACTIONS, isWorkflowExclusiveSlashAction, } from "./session-mode-view.js";
|
|
207
|
+
export function shouldLoadWorkflowBusinessTools(sessionMode) {
|
|
208
|
+
return sessionMode === "workflow";
|
|
209
|
+
}
|
|
210
|
+
export function shouldInjectGoalContinuation(sessionMode) {
|
|
211
|
+
return sessionMode === "workflow";
|
|
212
|
+
}
|
|
213
|
+
export function shouldAcceptOperatorSnapshot(sessionMode) {
|
|
214
|
+
return sessionMode === "workflow";
|
|
215
|
+
}
|
|
216
|
+
export const SESSION_MODE_ACTION_DENIED_MESSAGE = "this action is only available in workflow sessions";
|
|
217
|
+
/** Stable tool name used by tests and catalogs for the Explore subagent. */
|
|
218
|
+
export const PI_CHAT_AGENT_TOOL_NAME = OPERATOR_CHAT_AGENT_TOOL_NAME;
|
|
@@ -15,6 +15,7 @@ import { readdir, rm } from "node:fs/promises";
|
|
|
15
15
|
import { readJsonIfExists, writeSecureJson, } from "../../../infrastructure/console/app-data.js";
|
|
16
16
|
import { dispatchOperatorAction } from "../operator-actions.js";
|
|
17
17
|
import { authorizeOperatorChatTool } from "./tools.js";
|
|
18
|
+
import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, readStoredSessionMode, } from "./session-mode.js";
|
|
18
19
|
import { projectOperationForChat } from "./chat-event-store.js";
|
|
19
20
|
import { scrubSecrets } from "./explore-tools.js";
|
|
20
21
|
import { hashDeferredTurnPayload, parseDeferredTurns, } from "./deferred-turn.js";
|
|
@@ -106,6 +107,7 @@ export class ChatSessionStore {
|
|
|
106
107
|
...(typeof raw.workspaceId === "string" && raw.workspaceId
|
|
107
108
|
? { workspaceId: raw.workspaceId }
|
|
108
109
|
: {}),
|
|
110
|
+
sessionMode: readStoredSessionMode(raw.sessionMode),
|
|
109
111
|
deferredTurns: normalizedDeferredTurns,
|
|
110
112
|
...(normalizedDeferredTurns[0] ? { deferredTurn: normalizedDeferredTurns[0] } : {}),
|
|
111
113
|
deferredQueueRevision: Number.isSafeInteger(raw.deferredQueueRevision) && (raw.deferredQueueRevision ?? 0) >= 0
|
|
@@ -143,7 +145,17 @@ export class ChatSessionStore {
|
|
|
143
145
|
}
|
|
144
146
|
const records = await Promise.all(names
|
|
145
147
|
.filter((name) => name.endsWith(".json"))
|
|
146
|
-
.map((name) =>
|
|
148
|
+
.map(async (name) => {
|
|
149
|
+
try {
|
|
150
|
+
return await this.get(name.slice(0, -".json".length));
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
// Skip corrupt records (e.g. unknown sessionMode) but stay
|
|
154
|
+
// diagnosable: silent disappearance is untraceable otherwise.
|
|
155
|
+
process.stderr.write(`[console] chat session record skipped in list: ${name.slice(0, -".json".length)}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}));
|
|
147
159
|
return records
|
|
148
160
|
.filter((record) => Boolean(record) && (options?.includeArchived || record.state === "active"))
|
|
149
161
|
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
@@ -172,6 +184,7 @@ export class ChatSessionStore {
|
|
|
172
184
|
updatedAt: now,
|
|
173
185
|
modelProvider: input.modelProvider,
|
|
174
186
|
modelId: input.modelId,
|
|
187
|
+
sessionMode: input.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE,
|
|
175
188
|
messages: [],
|
|
176
189
|
toolInvocations: [],
|
|
177
190
|
};
|
|
@@ -497,9 +510,10 @@ export class ChatSessionStore {
|
|
|
497
510
|
/**
|
|
498
511
|
* Dispatch a Chat tool invocation into the operator action layer.
|
|
499
512
|
*
|
|
500
|
-
* Gate 3 (per-call):
|
|
501
|
-
* malicious or drifted session somehow requested a
|
|
502
|
-
*
|
|
513
|
+
* Gate 3 (per-call): authorizeChatSessionTool is re-checked here with the
|
|
514
|
+
* session mode. Even if a malicious or drifted session somehow requested a
|
|
515
|
+
* denied or workflow-exclusive tool, this gate refuses before any operator
|
|
516
|
+
* action runs (V12 / V15).
|
|
503
517
|
*
|
|
504
518
|
* High-risk actions (contractApply / runDag / etc.) are NOT registered as
|
|
505
519
|
* tools, so they never reach here; but we still deny defensively.
|
|
@@ -514,8 +528,10 @@ function redactHumanGateToken(value) {
|
|
|
514
528
|
delete clone.humanGateToken;
|
|
515
529
|
return clone;
|
|
516
530
|
}
|
|
517
|
-
export async function dispatchChatToolCall(ctx, toolName, args, clientRequestId) {
|
|
518
|
-
const decision =
|
|
531
|
+
export async function dispatchChatToolCall(ctx, toolName, args, clientRequestId, sessionMode) {
|
|
532
|
+
const decision = sessionMode === undefined
|
|
533
|
+
? authorizeOperatorChatTool(toolName)
|
|
534
|
+
: authorizeChatSessionTool(toolName, sessionMode);
|
|
519
535
|
if (!decision.ok) {
|
|
520
536
|
return {
|
|
521
537
|
ok: false,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* prompt / skill commands are merged via mergeSlashCommands with conflict
|
|
6
6
|
* priority: operator/web > extension > skill > prompt.
|
|
7
7
|
*/
|
|
8
|
+
import { isWorkflowExclusiveSlashAction } from "./session-mode-view.js";
|
|
8
9
|
export const SHORTCUTS = [
|
|
9
10
|
{
|
|
10
11
|
command: "/task",
|
|
@@ -138,7 +139,10 @@ export function mergeSlashCommands(dynamic = [], capabilities = {}) {
|
|
|
138
139
|
byName.set(key, { ...item, command: key });
|
|
139
140
|
}
|
|
140
141
|
};
|
|
142
|
+
const hideWorkflow = capabilities.sessionMode === "pi";
|
|
141
143
|
for (const item of SHORTCUTS) {
|
|
144
|
+
if (hideWorkflow && isWorkflowExclusiveSlashAction(item.action))
|
|
145
|
+
continue;
|
|
142
146
|
upsert({
|
|
143
147
|
command: item.command,
|
|
144
148
|
label: item.label,
|
|
@@ -148,6 +152,8 @@ export function mergeSlashCommands(dynamic = [], capabilities = {}) {
|
|
|
148
152
|
});
|
|
149
153
|
}
|
|
150
154
|
for (const item of WEB_SLASH_COMMANDS) {
|
|
155
|
+
if (hideWorkflow && isWorkflowExclusiveSlashAction(item.action))
|
|
156
|
+
continue;
|
|
151
157
|
upsert({
|
|
152
158
|
command: item.command,
|
|
153
159
|
label: item.label,
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator Chat — terminal management HTTP API (Console UI side).
|
|
3
|
+
*
|
|
4
|
+
* Endpoints (all under /api/operator/v1/chat):
|
|
5
|
+
* GET /terminals list terminal sessions (all owners)
|
|
6
|
+
* POST /terminals spawn { shell, name?, cwd?, ownerSessionId? }
|
|
7
|
+
* GET /terminals/:id session info + buffered output
|
|
8
|
+
* POST /terminals/:id/input { data } write to the PTY
|
|
9
|
+
* POST /terminals/:id/resize { cols, rows }
|
|
10
|
+
* POST /terminals/:id/interrupt send Ctrl+C
|
|
11
|
+
* POST /terminals/:id/kill stop and await full exit
|
|
12
|
+
* GET /terminals/:id/stream SSE: buffered replay + live chunks
|
|
13
|
+
*
|
|
14
|
+
* Mutating routes require the Console boot token (same gate as other chat
|
|
15
|
+
* mutations). The stream is read-only and uses the same origin checks as the
|
|
16
|
+
* chat event stream.
|
|
17
|
+
*/
|
|
18
|
+
import { TerminalSessionError, availableShellKinds, } from "./terminal-sessions.js";
|
|
19
|
+
function sendJson(res, status, body) {
|
|
20
|
+
const payload = JSON.stringify(body);
|
|
21
|
+
res.writeHead(status, {
|
|
22
|
+
"content-type": "application/json; charset=utf-8",
|
|
23
|
+
"cache-control": "no-store",
|
|
24
|
+
});
|
|
25
|
+
res.end(payload);
|
|
26
|
+
}
|
|
27
|
+
function mapTerminalError(res, error) {
|
|
28
|
+
if (error instanceof TerminalSessionError) {
|
|
29
|
+
const status = error.code === "TERMINAL_NOT_FOUND"
|
|
30
|
+
? 404
|
|
31
|
+
: error.code === "TERMINAL_EXITED" || error.code === "TERMINAL_SEND_ACTIVE"
|
|
32
|
+
? 409
|
|
33
|
+
: 500;
|
|
34
|
+
sendJson(res, status, {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: { code: error.code, message: error.message },
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
sendJson(res, 500, {
|
|
41
|
+
ok: false,
|
|
42
|
+
error: {
|
|
43
|
+
code: "TERMINAL_INTERNAL",
|
|
44
|
+
message: error instanceof Error ? error.message : String(error),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function readJsonBody(req, maxBytes = 512 * 1024) {
|
|
49
|
+
const chunks = [];
|
|
50
|
+
let total = 0;
|
|
51
|
+
for await (const chunk of req) {
|
|
52
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
53
|
+
total += buf.length;
|
|
54
|
+
if (total > maxBytes)
|
|
55
|
+
throw new Error("request body too large");
|
|
56
|
+
chunks.push(buf);
|
|
57
|
+
}
|
|
58
|
+
if (chunks.length === 0)
|
|
59
|
+
return {};
|
|
60
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
61
|
+
if (!raw.trim())
|
|
62
|
+
return {};
|
|
63
|
+
return JSON.parse(raw);
|
|
64
|
+
}
|
|
65
|
+
/** Route dispatcher; returns true when the request was handled. */
|
|
66
|
+
export async function handleTerminalRequest(req, res, deps, pathname) {
|
|
67
|
+
if (!pathname.startsWith("/api/operator/v1/chat/terminals"))
|
|
68
|
+
return false;
|
|
69
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
70
|
+
const rest = pathname.slice("/api/operator/v1/chat/terminals".length);
|
|
71
|
+
const parts = rest.split("/").filter(Boolean);
|
|
72
|
+
try {
|
|
73
|
+
// GET /terminals — list (+ available shell kinds for this platform)
|
|
74
|
+
if (method === "GET" && parts.length === 0) {
|
|
75
|
+
sendJson(res, 200, {
|
|
76
|
+
ok: true,
|
|
77
|
+
data: {
|
|
78
|
+
terminals: deps.terminals.list(),
|
|
79
|
+
availableShells: availableShellKinds().kinds,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
// POST /terminals — spawn
|
|
85
|
+
if (method === "POST" && parts.length === 0) {
|
|
86
|
+
const gate = deps.gateMutation(req);
|
|
87
|
+
if (!gate.ok) {
|
|
88
|
+
sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const body = await readJsonBody(req);
|
|
92
|
+
const kinds = availableShellKinds().kinds;
|
|
93
|
+
const shell = kinds.includes(body.shell)
|
|
94
|
+
? body.shell
|
|
95
|
+
: undefined;
|
|
96
|
+
if (!shell) {
|
|
97
|
+
sendJson(res, 400, {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: {
|
|
100
|
+
code: "INVALID_INPUT",
|
|
101
|
+
message: `shell must be one of: ${kinds.join(", ")} (available on this platform)`,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
const info = deps.terminals.spawn({
|
|
107
|
+
ownerSessionId: typeof body.ownerSessionId === "string" && body.ownerSessionId
|
|
108
|
+
? body.ownerSessionId
|
|
109
|
+
: "console-ui",
|
|
110
|
+
name: typeof body.name === "string" && body.name ? body.name : `${shell}-ui`,
|
|
111
|
+
shell: shell,
|
|
112
|
+
cwd: typeof body.cwd === "string" && body.cwd
|
|
113
|
+
? body.cwd
|
|
114
|
+
: (deps.defaultCwd ?? undefined),
|
|
115
|
+
cols: typeof body.cols === "number" ? body.cols : undefined,
|
|
116
|
+
rows: typeof body.rows === "number" ? body.rows : undefined,
|
|
117
|
+
});
|
|
118
|
+
sendJson(res, 201, { ok: true, data: info });
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const sessionId = parts[0];
|
|
122
|
+
if (!sessionId)
|
|
123
|
+
return false;
|
|
124
|
+
const sub = parts[1] ?? "";
|
|
125
|
+
// GET /terminals/:id — info + buffered output
|
|
126
|
+
if (method === "GET" && parts.length === 1) {
|
|
127
|
+
const info = deps.terminals.get(sessionId);
|
|
128
|
+
const output = deps.terminals.readAll(sessionId);
|
|
129
|
+
sendJson(res, 200, { ok: true, data: { ...info, output } });
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
// GET /terminals/:id/stream — SSE
|
|
133
|
+
if (method === "GET" && sub === "stream") {
|
|
134
|
+
// Validate existence first (404 before opening the stream).
|
|
135
|
+
const info = deps.terminals.get(sessionId);
|
|
136
|
+
void info;
|
|
137
|
+
res.writeHead(200, {
|
|
138
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
139
|
+
"cache-control": "no-cache, no-store",
|
|
140
|
+
connection: "keep-alive",
|
|
141
|
+
"x-accel-buffering": "no",
|
|
142
|
+
});
|
|
143
|
+
const writeFrame = (event, data) => {
|
|
144
|
+
if (res.writableEnded)
|
|
145
|
+
return;
|
|
146
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
147
|
+
};
|
|
148
|
+
const buffered = deps.terminals.readAll(sessionId);
|
|
149
|
+
writeFrame("snapshot", {
|
|
150
|
+
truncated: buffered.truncated,
|
|
151
|
+
totalOutputChars: buffered.totalOutputChars,
|
|
152
|
+
text: buffered.text,
|
|
153
|
+
info: deps.terminals.get(sessionId),
|
|
154
|
+
});
|
|
155
|
+
const unsubscribe = deps.terminals.subscribe(sessionId, (chunk) => {
|
|
156
|
+
writeFrame("data", { chunk });
|
|
157
|
+
});
|
|
158
|
+
const heartbeat = setInterval(() => {
|
|
159
|
+
if (!res.writableEnded)
|
|
160
|
+
res.write(": heartbeat\n\n");
|
|
161
|
+
}, 15_000);
|
|
162
|
+
res.once("close", () => {
|
|
163
|
+
clearInterval(heartbeat);
|
|
164
|
+
unsubscribe();
|
|
165
|
+
});
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
// Everything else mutates → gate first.
|
|
169
|
+
const gate = deps.gateMutation(req);
|
|
170
|
+
if (!gate.ok) {
|
|
171
|
+
sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (method === "POST" && sub === "input" && parts.length === 2) {
|
|
175
|
+
const body = await readJsonBody(req);
|
|
176
|
+
if (typeof body.data !== "string") {
|
|
177
|
+
sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "data (string) is required" } });
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
deps.terminals.send(sessionId, body.data);
|
|
181
|
+
sendJson(res, 200, { ok: true });
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
if (method === "POST" && sub === "resize" && parts.length === 2) {
|
|
185
|
+
const body = await readJsonBody(req);
|
|
186
|
+
const cols = Number(body.cols);
|
|
187
|
+
const rows = Number(body.rows);
|
|
188
|
+
if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols < 2 || rows < 2 || cols > 500 || rows > 200) {
|
|
189
|
+
sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "cols and rows (integers 2..500/200) are required" } });
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
deps.terminals.resize(sessionId, cols, rows);
|
|
193
|
+
sendJson(res, 200, { ok: true });
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (method === "POST" && sub === "interrupt" && parts.length === 2) {
|
|
197
|
+
deps.terminals.interrupt(sessionId);
|
|
198
|
+
sendJson(res, 200, { ok: true });
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (method === "POST" && sub === "kill" && parts.length === 2) {
|
|
202
|
+
await deps.terminals.kill(sessionId);
|
|
203
|
+
sendJson(res, 200, { ok: true });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: `unknown terminal route: ${method} ${pathname}` } });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (!res.headersSent)
|
|
211
|
+
mapTerminalError(res, error);
|
|
212
|
+
else
|
|
213
|
+
res.end();
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|