@co0ontty/wand 4.79.0 → 4.80.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/README.md +18 -6
- package/dist/agent-dispatch.d.ts +3 -0
- package/dist/agent-dispatch.js +5 -0
- package/dist/ai-team-runner.js +4 -2
- package/dist/ai-team-types.d.ts +34 -0
- package/dist/ai-team-types.js +12 -1
- package/dist/build-info.json +3 -3
- package/dist/config.d.ts +4 -3
- package/dist/config.js +25 -0
- package/dist/git-quick-commit.d.ts +9 -8
- package/dist/git-quick-commit.js +123 -8
- package/dist/models.d.ts +3 -0
- package/dist/models.js +24 -2
- package/dist/process-manager.js +16 -3
- package/dist/provider-cli-updater.d.ts +1 -1
- package/dist/provider-cli-updater.js +12 -1
- package/dist/server-ai-team-routes.js +4 -1
- package/dist/server-employee-routes.d.ts +22 -0
- package/dist/server-employee-routes.js +277 -0
- package/dist/server-session-routes.js +38 -13
- package/dist/server-settings-routes.js +5 -0
- package/dist/server-task-routes.d.ts +2 -0
- package/dist/server-task-routes.js +174 -22
- package/dist/server-update-routes.js +1 -1
- package/dist/server-workspace-routes.js +3 -0
- package/dist/server.js +16 -3
- package/dist/session-ai-context.d.ts +7 -2
- package/dist/session-ai-context.js +36 -9
- package/dist/session-provider.d.ts +6 -1
- package/dist/session-provider.js +27 -1
- package/dist/session-transport.js +4 -0
- package/dist/silicon-employee-dispatch.d.ts +9 -0
- package/dist/silicon-employee-dispatch.js +13 -0
- package/dist/silicon-employee-draft.d.ts +36 -0
- package/dist/silicon-employee-draft.js +146 -0
- package/dist/storage.d.ts +19 -2
- package/dist/storage.js +158 -7
- package/dist/structured-gemini-adapter.d.ts +40 -0
- package/dist/structured-gemini-adapter.js +198 -0
- package/dist/structured-provider-common.d.ts +2 -1
- package/dist/structured-provider-common.js +7 -2
- package/dist/structured-session-manager.d.ts +10 -0
- package/dist/structured-session-manager.js +106 -14
- package/dist/system-ai.js +1 -1
- package/dist/system-employee.d.ts +35 -0
- package/dist/system-employee.js +81 -0
- package/dist/task-types.d.ts +8 -1
- package/dist/task-types.js +7 -0
- package/dist/types.d.ts +23 -4
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +64 -64
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +3 -0
- package/dist/web-ui/embedded-assets.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +2 -1
- package/dist/web-ui/provider-identity.js +10 -1
- package/dist/web-ui/session-activity.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { startStructuredCli } from "./structured-exec-pump.js";
|
|
3
|
+
import { asRecord } from "./structured-content.js";
|
|
4
|
+
import { promptWithSystemFallback } from "./structured-provider-common.js";
|
|
5
|
+
/**
|
|
6
|
+
* Gemini CLI 的会话写在 `~/.gemini/tmp/<project_hash>/chats/`。ID 找不回来时 headless
|
|
7
|
+
* 会以 42 退出并打印下面这句,识别出来才能丢掉失效的 resume ID 重开一轮
|
|
8
|
+
* (与 Claude / Pi 的同类兜底一致)。
|
|
9
|
+
*/
|
|
10
|
+
export function isMissingGeminiSession(error) {
|
|
11
|
+
if (!error)
|
|
12
|
+
return false;
|
|
13
|
+
return /Invalid session identifier|No previous sessions found/i.test(error);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* approval mode 映射。结构化 runner 没有权限桥,`default` 下需要确认的工具调用会被
|
|
17
|
+
* 直接拒绝;`yolo` 才是能连续干活的那一档。
|
|
18
|
+
*/
|
|
19
|
+
export function geminiApprovalMode(session) {
|
|
20
|
+
if (session.autoApprovePermissions === true
|
|
21
|
+
|| session.mode === "full-access"
|
|
22
|
+
|| session.mode === "managed") {
|
|
23
|
+
return "yolo";
|
|
24
|
+
}
|
|
25
|
+
if (session.mode === "auto-edit")
|
|
26
|
+
return "auto_edit";
|
|
27
|
+
return "default";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `-p ""` 只把 CLI 钉在 headless 模式,真正的 prompt 走 stdin(Gemini CLI 把 stdin
|
|
31
|
+
* 内容与 `-p` 内容用空行拼接,`-p` 为空时只剩 stdin)。这样长消息不撞 argv 上限,
|
|
32
|
+
* 也不会出现在 `ps` 里。
|
|
33
|
+
*
|
|
34
|
+
* `--skip-trust` 必须带:headless 模式下 Gemini CLI 会检查工作区信任状态,未信任的
|
|
35
|
+
* 目录会直接以 55 退出,并把 `--approval-mode` 降级成 `default`。Wand 是在用户自己
|
|
36
|
+
* 选定的工作目录里代表用户启动这个 CLI,与其它 provider 的 auto-approve 语义一致。
|
|
37
|
+
*/
|
|
38
|
+
export function buildGeminiArgs(session) {
|
|
39
|
+
const args = ["-p", "", "--output-format", "stream-json", "--skip-trust"];
|
|
40
|
+
const model = session.selectedModel?.trim();
|
|
41
|
+
if (model && model !== "default")
|
|
42
|
+
args.push("--model", model);
|
|
43
|
+
args.push("--approval-mode", geminiApprovalMode(session));
|
|
44
|
+
if (session.claudeSessionId)
|
|
45
|
+
args.push("--resume", session.claudeSessionId);
|
|
46
|
+
return args;
|
|
47
|
+
}
|
|
48
|
+
const GEMINI_TOOL_NAMES = {
|
|
49
|
+
run_shell_command: "Bash",
|
|
50
|
+
shell: "Bash",
|
|
51
|
+
read_file: "Read",
|
|
52
|
+
read_many_files: "Read",
|
|
53
|
+
write_file: "Write",
|
|
54
|
+
replace: "Edit",
|
|
55
|
+
edit: "Edit",
|
|
56
|
+
glob: "Glob",
|
|
57
|
+
list_directory: "Glob",
|
|
58
|
+
grep_search: "Grep",
|
|
59
|
+
search_file_content: "Grep",
|
|
60
|
+
web_search: "WebSearch",
|
|
61
|
+
google_web_search: "WebSearch",
|
|
62
|
+
web_fetch: "WebFetch",
|
|
63
|
+
write_todos: "TodoWrite",
|
|
64
|
+
todo_write: "TodoWrite",
|
|
65
|
+
ask_user: "AskUserQuestion",
|
|
66
|
+
activate_skill: "Skill",
|
|
67
|
+
};
|
|
68
|
+
export function geminiToolName(name) {
|
|
69
|
+
return GEMINI_TOOL_NAMES[name.toLowerCase()] ?? `Gemini/${name}`;
|
|
70
|
+
}
|
|
71
|
+
function applyGeminiStats(state, stats) {
|
|
72
|
+
const input = typeof stats.input_tokens === "number" ? stats.input_tokens : 0;
|
|
73
|
+
const cached = typeof stats.cached === "number" ? stats.cached : 0;
|
|
74
|
+
const output = typeof stats.output_tokens === "number" ? stats.output_tokens : 0;
|
|
75
|
+
if (!input && !output)
|
|
76
|
+
return;
|
|
77
|
+
state.usage = {
|
|
78
|
+
inputTokens: Math.max(input - cached, 0),
|
|
79
|
+
outputTokens: output,
|
|
80
|
+
cacheReadInputTokens: cached,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function geminiToolOutput(event) {
|
|
84
|
+
if (typeof event.output === "string" && event.output)
|
|
85
|
+
return event.output;
|
|
86
|
+
const error = asRecord(event.error);
|
|
87
|
+
return typeof error?.message === "string" ? error.message : "";
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Apply one Gemini CLI `--output-format stream-json` event.
|
|
91
|
+
*
|
|
92
|
+
* assistant `message` 事件带 `delta: true`,每个 chunk 是增量而不是快照,所以文本是
|
|
93
|
+
* 追加而不是替换。tool_use / tool_result 由 CLI 直接按 tool_id 配对,不需要 upsert。
|
|
94
|
+
*/
|
|
95
|
+
export function applyGeminiEvent(state, event) {
|
|
96
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
97
|
+
if (type === "init") {
|
|
98
|
+
if (typeof event.session_id === "string" && event.session_id)
|
|
99
|
+
state.sessionId = event.session_id;
|
|
100
|
+
if (typeof event.model === "string" && event.model)
|
|
101
|
+
state.model = event.model;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (type === "message") {
|
|
105
|
+
const content = typeof event.content === "string" ? event.content : "";
|
|
106
|
+
if (event.role !== "assistant" || !content)
|
|
107
|
+
return null;
|
|
108
|
+
const previous = state.blocks.at(-1);
|
|
109
|
+
if (previous?.type === "text")
|
|
110
|
+
previous.text += content;
|
|
111
|
+
else
|
|
112
|
+
state.blocks.push({ type: "text", text: content });
|
|
113
|
+
state.result += content;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (type === "tool_use") {
|
|
117
|
+
const id = typeof event.tool_id === "string" && event.tool_id ? event.tool_id : "tool";
|
|
118
|
+
const name = typeof event.tool_name === "string" && event.tool_name ? event.tool_name : "tool";
|
|
119
|
+
state.blocks.push({ type: "tool_use", id, name: geminiToolName(name), input: asRecord(event.parameters) ?? {} });
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (type === "tool_result") {
|
|
123
|
+
const id = typeof event.tool_id === "string" && event.tool_id ? event.tool_id : "unknown";
|
|
124
|
+
state.blocks.push({
|
|
125
|
+
type: "tool_result",
|
|
126
|
+
tool_use_id: id,
|
|
127
|
+
content: geminiToolOutput(event),
|
|
128
|
+
is_error: event.status === "error",
|
|
129
|
+
});
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (type === "error") {
|
|
133
|
+
// warning 只是提示(例如循环检测),不应把整轮判成失败。
|
|
134
|
+
if (event.severity !== "error")
|
|
135
|
+
return null;
|
|
136
|
+
return typeof event.message === "string" && event.message ? event.message : "Gemini CLI execution failed";
|
|
137
|
+
}
|
|
138
|
+
if (type === "result") {
|
|
139
|
+
const stats = asRecord(event.stats);
|
|
140
|
+
if (stats)
|
|
141
|
+
applyGeminiStats(state, stats);
|
|
142
|
+
if (event.status === "error") {
|
|
143
|
+
const error = asRecord(event.error);
|
|
144
|
+
const message = typeof error?.message === "string" ? error.message : "";
|
|
145
|
+
return message || "Gemini CLI execution failed";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
export class GeminiRunner {
|
|
151
|
+
spawnProcess;
|
|
152
|
+
execHost;
|
|
153
|
+
constructor(spawnProcess = spawn, execHost) {
|
|
154
|
+
this.spawnProcess = spawnProcess;
|
|
155
|
+
this.execHost = execHost;
|
|
156
|
+
}
|
|
157
|
+
start(context, observer) {
|
|
158
|
+
const state = {
|
|
159
|
+
blocks: [],
|
|
160
|
+
result: "",
|
|
161
|
+
sessionId: context.session.claudeSessionId,
|
|
162
|
+
model: context.session.selectedModel ?? context.session.structuredState?.model,
|
|
163
|
+
phase: "responding",
|
|
164
|
+
};
|
|
165
|
+
let primaryError = null;
|
|
166
|
+
return startStructuredCli({
|
|
167
|
+
sessionId: context.session.id,
|
|
168
|
+
file: "gemini",
|
|
169
|
+
args: buildGeminiArgs(context.session),
|
|
170
|
+
cwd: context.session.cwd,
|
|
171
|
+
env: context.env,
|
|
172
|
+
stdinData: promptWithSystemFallback(context.session, context.prompt),
|
|
173
|
+
observer,
|
|
174
|
+
execHost: this.execHost,
|
|
175
|
+
spawnProcess: this.spawnProcess,
|
|
176
|
+
createState: () => state,
|
|
177
|
+
processLine: (line) => {
|
|
178
|
+
if (!observer.isActive() || !line.trim())
|
|
179
|
+
return;
|
|
180
|
+
try {
|
|
181
|
+
const event = JSON.parse(line);
|
|
182
|
+
observer.onEvent?.(event);
|
|
183
|
+
primaryError = applyGeminiEvent(state, event) ?? primaryError;
|
|
184
|
+
observer.onUpdate(state);
|
|
185
|
+
}
|
|
186
|
+
catch { /* Gemini stdout is NDJSON; ignore non-protocol noise. */ }
|
|
187
|
+
},
|
|
188
|
+
finalize: (ctx, exitCode, signal, spawnError) => ({
|
|
189
|
+
state,
|
|
190
|
+
exitCode,
|
|
191
|
+
signal,
|
|
192
|
+
stderr: ctx.stderr,
|
|
193
|
+
primaryError,
|
|
194
|
+
...(spawnError ? { spawnError } : {}),
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { SessionProvider, SessionRunner, SessionSnapshot, StructuredSessionState, WandConfig } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* 把会话级系统提示交给 CLI 的开关。Claude / Qoder / Pi 用 `--append-system-prompt`,
|
|
4
|
-
* Grok 用 `--rules`;Codex / OpenCode
|
|
4
|
+
* Grok 用 `--rules`;Codex / OpenCode / Gemini 没有这个入口,只能退回并入本轮消息
|
|
5
|
+
* (Gemini 只有 `GEMINI_SYSTEM_MD`,那是整体替换而非追加)。
|
|
5
6
|
* 各 provider 的支持情况由 `--help` 实测得出,新增 provider 前先验。
|
|
6
7
|
*/
|
|
7
8
|
export declare function systemPromptFlag(provider: SessionProvider | null | undefined): string | null;
|
|
@@ -2,7 +2,8 @@ import { shellQuote } from "./shell-quote.js";
|
|
|
2
2
|
const NATIVE_THINKING_EFFORT = /^(claude|codex|opencode|grok|qoder|pi):[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
3
3
|
/**
|
|
4
4
|
* 把会话级系统提示交给 CLI 的开关。Claude / Qoder / Pi 用 `--append-system-prompt`,
|
|
5
|
-
* Grok 用 `--rules`;Codex / OpenCode
|
|
5
|
+
* Grok 用 `--rules`;Codex / OpenCode / Gemini 没有这个入口,只能退回并入本轮消息
|
|
6
|
+
* (Gemini 只有 `GEMINI_SYSTEM_MD`,那是整体替换而非追加)。
|
|
6
7
|
* 各 provider 的支持情况由 `--help` 实测得出,新增 provider 前先验。
|
|
7
8
|
*/
|
|
8
9
|
export function systemPromptFlag(provider) {
|
|
@@ -82,7 +83,9 @@ export function isStructuredRunnerForProvider(provider, runner) {
|
|
|
82
83
|
return runner === "grok-cli-headless";
|
|
83
84
|
if (provider === "qoder")
|
|
84
85
|
return runner === "qoder-cli-print";
|
|
85
|
-
|
|
86
|
+
if (provider === "pi")
|
|
87
|
+
return runner === "pi-cli-json";
|
|
88
|
+
return runner === "gemini-cli-json";
|
|
86
89
|
}
|
|
87
90
|
export function defaultStructuredRunner(provider, configuredClaudeRunner = "cli") {
|
|
88
91
|
if (provider === "codex")
|
|
@@ -95,6 +98,8 @@ export function defaultStructuredRunner(provider, configuredClaudeRunner = "cli"
|
|
|
95
98
|
return "qoder-cli-print";
|
|
96
99
|
if (provider === "pi")
|
|
97
100
|
return "pi-cli-json";
|
|
101
|
+
if (provider === "gemini")
|
|
102
|
+
return "gemini-cli-json";
|
|
98
103
|
return configuredClaudeRunner === "sdk" ? "claude-sdk" : "claude-cli-print";
|
|
99
104
|
}
|
|
100
105
|
export function resolveStructuredRunner(provider, requestedRunner, configuredClaudeRunner = "cli") {
|
|
@@ -3,6 +3,7 @@ import { type WorktreeSetupSpec } from "./git-worktree.js";
|
|
|
3
3
|
import { SessionLogger } from "./session-logger.js";
|
|
4
4
|
import { WandStorage } from "./storage.js";
|
|
5
5
|
import { ConversationTurn, ExecutionMode, ProcessEvent, SessionProvider, SessionRunner, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
|
|
6
|
+
import type { WandTaskAgent } from "./task-types.js";
|
|
6
7
|
import { type StructuredExecHost } from "./structured-exec-host.js";
|
|
7
8
|
import type { StructuredRunnerAdapter } from "./structured-runner.js";
|
|
8
9
|
export interface StructuredSessionManagerRunners {
|
|
@@ -12,6 +13,7 @@ export interface StructuredSessionManagerRunners {
|
|
|
12
13
|
grok?: StructuredRunnerAdapter;
|
|
13
14
|
qoder?: StructuredRunnerAdapter;
|
|
14
15
|
pi?: StructuredRunnerAdapter;
|
|
16
|
+
gemini?: StructuredRunnerAdapter;
|
|
15
17
|
}
|
|
16
18
|
interface CreateStructuredSessionOptions {
|
|
17
19
|
cwd: string;
|
|
@@ -26,6 +28,11 @@ interface CreateStructuredSessionOptions {
|
|
|
26
28
|
thinkingEffort?: SessionSnapshot["thinkingEffort"];
|
|
27
29
|
sessionSource?: SessionSource;
|
|
28
30
|
automationId?: string;
|
|
31
|
+
employeeId?: string;
|
|
32
|
+
employeeName?: string;
|
|
33
|
+
employeeAvatar?: string;
|
|
34
|
+
employeeCandidates?: WandTaskAgent[];
|
|
35
|
+
employeeCandidateIndex?: number;
|
|
29
36
|
/** 会话级系统提示(团队 / 自动化的角色与规则);走 provider 的系统提示通道。 */
|
|
30
37
|
systemPrompt?: string;
|
|
31
38
|
/** 所属工作空间 ID(多标签 / 分屏项目)。 */
|
|
@@ -108,6 +115,7 @@ export declare class StructuredSessionManager {
|
|
|
108
115
|
private readonly grokRunner;
|
|
109
116
|
private readonly qoderRunner;
|
|
110
117
|
private readonly piRunner;
|
|
118
|
+
private readonly geminiRunner;
|
|
111
119
|
/** Structured CLI runs that were mid-flight when the previous web process died. */
|
|
112
120
|
private pendingRecoveryIds;
|
|
113
121
|
private detachedRecoveryPromise;
|
|
@@ -154,6 +162,8 @@ export declare class StructuredSessionManager {
|
|
|
154
162
|
automationId: string;
|
|
155
163
|
title: string;
|
|
156
164
|
}): SessionSnapshot;
|
|
165
|
+
/** Only an unstarted employee conversation may change CLI after a failed first turn. */
|
|
166
|
+
private retryEmployeeCandidate;
|
|
157
167
|
/** 往转发会话里追加别的参与者的发言。 */
|
|
158
168
|
appendRelayTurns(id: string, turns: ConversationTurn[]): SessionSnapshot | null;
|
|
159
169
|
sendMessage(id: string, input: string, opts?: {
|
|
@@ -4,12 +4,13 @@ import { prepareSessionWorktree } from "./git-worktree.js";
|
|
|
4
4
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
5
5
|
import { buildChildEnv } from "./env-utils.js";
|
|
6
6
|
import { getErrorMessage } from "./error-utils.js";
|
|
7
|
+
import { getDefaultModelForProvider } from "./config.js";
|
|
7
8
|
import { recordIterationPrompt } from "./iteration-log.js";
|
|
8
9
|
import { signalNameFromNumber } from "./signal-utils.js";
|
|
9
10
|
import { resolveSdkClaudeBinary } from "./claude-sdk-runner.js";
|
|
10
11
|
import { provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
|
|
11
12
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
12
|
-
import { isSessionProvider } from "./session-provider.js";
|
|
13
|
+
import { isSessionProvider, providerCliCommand } from "./session-provider.js";
|
|
13
14
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
14
15
|
import { CodexRunner } from "./structured-codex-adapter.js";
|
|
15
16
|
import { CodexProtocolReducer } from "./structured-codex-protocol.js";
|
|
@@ -21,6 +22,7 @@ import { OpenCodeRunner, applyOpenCodeEvent } from "./structured-opencode-adapte
|
|
|
21
22
|
import { GrokRunner, applyGrokEvent } from "./structured-grok-adapter.js";
|
|
22
23
|
import { QoderRunner } from "./structured-qoder-adapter.js";
|
|
23
24
|
import { PiRunner, applyPiEvent, isMissingPiSession } from "./structured-pi-adapter.js";
|
|
25
|
+
import { GeminiRunner, applyGeminiEvent, isMissingGeminiSession } from "./structured-gemini-adapter.js";
|
|
24
26
|
import { structuredRunId, } from "./structured-exec-host.js";
|
|
25
27
|
import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, } from "./structured-provider-common.js";
|
|
26
28
|
import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
|
|
@@ -30,6 +32,13 @@ class PersistedStructuredRunnerError extends Error {
|
|
|
30
32
|
this.name = "PersistedStructuredRunnerError";
|
|
31
33
|
}
|
|
32
34
|
}
|
|
35
|
+
/** The child process never started, so retrying the same input with another CLI cannot duplicate work. */
|
|
36
|
+
class UnacceptedStructuredSpawnError extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "UnacceptedStructuredSpawnError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
33
42
|
function buildReplayProcessor(session) {
|
|
34
43
|
const runner = session.runner;
|
|
35
44
|
if (runner === "codex-cli-exec") {
|
|
@@ -90,7 +99,7 @@ function buildReplayProcessor(session) {
|
|
|
90
99
|
};
|
|
91
100
|
return processor;
|
|
92
101
|
}
|
|
93
|
-
// grok-cli-headless / opencode-cli-run / pi-cli-json share the same shape.
|
|
102
|
+
// grok-cli-headless / opencode-cli-run / pi-cli-json / gemini-cli-json share the same shape.
|
|
94
103
|
const state = {
|
|
95
104
|
blocks: [],
|
|
96
105
|
result: "",
|
|
@@ -117,7 +126,9 @@ function buildReplayProcessor(session) {
|
|
|
117
126
|
? applyGrokEvent(state, event)
|
|
118
127
|
: runner === "opencode-cli-run"
|
|
119
128
|
? applyOpenCodeEvent(state, event)
|
|
120
|
-
:
|
|
129
|
+
: runner === "gemini-cli-json"
|
|
130
|
+
? applyGeminiEvent(state, event)
|
|
131
|
+
: applyPiEvent(state, event);
|
|
121
132
|
if (error)
|
|
122
133
|
processor.primaryError = error;
|
|
123
134
|
return true;
|
|
@@ -132,6 +143,7 @@ function recoveredCommandLabel(runner) {
|
|
|
132
143
|
case "grok-cli-headless": return "grok -p --output-format streaming-json";
|
|
133
144
|
case "qoder-cli-print": return "qodercli -p --output-format stream-json";
|
|
134
145
|
case "pi-cli-json": return "pi --mode json";
|
|
146
|
+
case "gemini-cli-json": return "gemini -p --output-format stream-json";
|
|
135
147
|
default: return "claude -p";
|
|
136
148
|
}
|
|
137
149
|
}
|
|
@@ -211,6 +223,14 @@ function buildStructuredOutputPayload(snapshot) {
|
|
|
211
223
|
queuedMessageSkills: snapshot.queuedMessageSkills,
|
|
212
224
|
sessionKind: "structured",
|
|
213
225
|
structuredState: snapshot.structuredState,
|
|
226
|
+
provider: snapshot.provider,
|
|
227
|
+
runner: snapshot.runner,
|
|
228
|
+
selectedModel: snapshot.selectedModel,
|
|
229
|
+
thinkingEffort: snapshot.thinkingEffort,
|
|
230
|
+
employeeId: snapshot.employeeId,
|
|
231
|
+
employeeName: snapshot.employeeName,
|
|
232
|
+
employeeAvatar: snapshot.employeeAvatar,
|
|
233
|
+
employeeCandidateIndex: snapshot.employeeCandidateIndex,
|
|
214
234
|
title: snapshot.title,
|
|
215
235
|
description: snapshot.description,
|
|
216
236
|
summary: snapshot.description ?? snapshot.summary,
|
|
@@ -426,6 +446,7 @@ export class StructuredSessionManager {
|
|
|
426
446
|
grokRunner;
|
|
427
447
|
qoderRunner;
|
|
428
448
|
piRunner;
|
|
449
|
+
geminiRunner;
|
|
429
450
|
/** Structured CLI runs that were mid-flight when the previous web process died. */
|
|
430
451
|
pendingRecoveryIds = [];
|
|
431
452
|
detachedRecoveryPromise = null;
|
|
@@ -444,6 +465,7 @@ export class StructuredSessionManager {
|
|
|
444
465
|
this.grokRunner = runners.grok ?? new GrokRunner(undefined, this.execHost);
|
|
445
466
|
this.qoderRunner = runners.qoder ?? new QoderRunner(undefined, this.execHost);
|
|
446
467
|
this.piRunner = runners.pi ?? new PiRunner(undefined, this.execHost);
|
|
468
|
+
this.geminiRunner = runners.gemini ?? new GeminiRunner(undefined, this.execHost);
|
|
447
469
|
for (const snapshot of this.storage.loadSessions()) {
|
|
448
470
|
if ((snapshot.sessionKind ?? "pty") !== "structured")
|
|
449
471
|
continue;
|
|
@@ -1131,7 +1153,7 @@ export class StructuredSessionManager {
|
|
|
1131
1153
|
input,
|
|
1132
1154
|
cwd: session.cwd,
|
|
1133
1155
|
language: this.config.language,
|
|
1134
|
-
ai: resolveSystemAiContext(session, this.config),
|
|
1156
|
+
ai: resolveSystemAiContext(session, this.config, this.storage.getSystemSiliconEmployee()),
|
|
1135
1157
|
onGenerating: (generating) => {
|
|
1136
1158
|
if (!this.disposed)
|
|
1137
1159
|
this.setSessionTopicGenerating(id, generating);
|
|
@@ -1168,6 +1190,11 @@ export class StructuredSessionManager {
|
|
|
1168
1190
|
sessionKind: "structured",
|
|
1169
1191
|
sessionSource: options.sessionSource ?? "interactive",
|
|
1170
1192
|
automationId: options.automationId,
|
|
1193
|
+
employeeId: options.employeeId,
|
|
1194
|
+
employeeName: options.employeeName,
|
|
1195
|
+
employeeAvatar: options.employeeAvatar,
|
|
1196
|
+
employeeCandidates: options.employeeCandidates,
|
|
1197
|
+
employeeCandidateIndex: options.employeeCandidateIndex,
|
|
1171
1198
|
systemPrompt: options.systemPrompt?.trim() || null,
|
|
1172
1199
|
workspaceId: options.workspaceId,
|
|
1173
1200
|
workspaceTaskId: options.workspaceTaskId,
|
|
@@ -1183,9 +1210,11 @@ export class StructuredSessionManager {
|
|
|
1183
1210
|
? "qodercli -p --output-format stream-json"
|
|
1184
1211
|
: provider === "pi"
|
|
1185
1212
|
? "pi --mode json --print"
|
|
1186
|
-
:
|
|
1187
|
-
? "
|
|
1188
|
-
: "claude
|
|
1213
|
+
: provider === "gemini"
|
|
1214
|
+
? "gemini -p --output-format stream-json"
|
|
1215
|
+
: runner === "claude-sdk"
|
|
1216
|
+
? "claude-agent-sdk (stream-json)"
|
|
1217
|
+
: "claude -p --output-format stream-json",
|
|
1189
1218
|
cwd: worktreeSetup?.cwd ?? baseCwd,
|
|
1190
1219
|
mode: options.mode,
|
|
1191
1220
|
worktreeEnabled: Boolean(worktreeSetup),
|
|
@@ -1238,6 +1267,49 @@ export class StructuredSessionManager {
|
|
|
1238
1267
|
this.emitStructuredSnapshot(titled);
|
|
1239
1268
|
return titled;
|
|
1240
1269
|
}
|
|
1270
|
+
/** Only an unstarted employee conversation may change CLI after a failed first turn. */
|
|
1271
|
+
retryEmployeeCandidate(id, original, prompt, error, skills) {
|
|
1272
|
+
const candidates = original.employeeCandidates ?? [];
|
|
1273
|
+
const nextIndex = (original.employeeCandidateIndex ?? 0) + 1;
|
|
1274
|
+
if (!original.employeeId || (original.messages?.length ?? 0) !== 0 || nextIndex >= candidates.length)
|
|
1275
|
+
return null;
|
|
1276
|
+
const current = this.sessions.get(id);
|
|
1277
|
+
if (!current || (current.status !== "running" && current.status !== "failed"))
|
|
1278
|
+
return null;
|
|
1279
|
+
if ((current.messages ?? []).length !== 1 || current.messages?.[0]?.role !== "user")
|
|
1280
|
+
return null;
|
|
1281
|
+
if (!(error instanceof UnacceptedStructuredSpawnError))
|
|
1282
|
+
return null;
|
|
1283
|
+
const next = candidates[nextIndex];
|
|
1284
|
+
const runner = resolveStructuredRunner(next.provider, undefined, this.config.structuredRunner);
|
|
1285
|
+
const model = next.model === "default"
|
|
1286
|
+
? getDefaultModelForProvider(this.config, next.provider) || null
|
|
1287
|
+
: next.model;
|
|
1288
|
+
const retry = {
|
|
1289
|
+
...current,
|
|
1290
|
+
provider: next.provider,
|
|
1291
|
+
runner,
|
|
1292
|
+
command: providerCliCommand(next.provider),
|
|
1293
|
+
mode: next.mode,
|
|
1294
|
+
autoApprovePermissions: shouldAutoApproveForMode(next.mode),
|
|
1295
|
+
selectedModel: model,
|
|
1296
|
+
thinkingEffort: next.thinkingEffort,
|
|
1297
|
+
employeeCandidateIndex: nextIndex,
|
|
1298
|
+
claudeSessionId: null,
|
|
1299
|
+
messages: [],
|
|
1300
|
+
status: "idle",
|
|
1301
|
+
exitCode: null,
|
|
1302
|
+
endedAt: null,
|
|
1303
|
+
structuredState: {
|
|
1304
|
+
...defaultStructuredState(next.provider, runner),
|
|
1305
|
+
model: model ?? undefined,
|
|
1306
|
+
},
|
|
1307
|
+
};
|
|
1308
|
+
this.sessions.set(id, retry);
|
|
1309
|
+
this.saveAuthoritativeSession(retry);
|
|
1310
|
+
this.emitStructuredSnapshot(retry);
|
|
1311
|
+
return this.sendMessage(id, prompt, { skills });
|
|
1312
|
+
}
|
|
1241
1313
|
/** 往转发会话里追加别的参与者的发言。 */
|
|
1242
1314
|
appendRelayTurns(id, turns) {
|
|
1243
1315
|
const session = this.sessions.get(id);
|
|
@@ -1405,7 +1477,8 @@ export class StructuredSessionManager {
|
|
|
1405
1477
|
content: [{ type: "text", text: prompt }],
|
|
1406
1478
|
};
|
|
1407
1479
|
const requestId = randomUUID();
|
|
1408
|
-
if (session.provider === "pi" && isMissingPiSession(session.structuredState?.lastError, session.claudeSessionId))
|
|
1480
|
+
if ((session.provider === "pi" && isMissingPiSession(session.structuredState?.lastError, session.claudeSessionId))
|
|
1481
|
+
|| (session.provider === "gemini" && isMissingGeminiSession(session.structuredState?.lastError))) {
|
|
1409
1482
|
session = { ...session, claudeSessionId: null };
|
|
1410
1483
|
}
|
|
1411
1484
|
const updated = {
|
|
@@ -1472,6 +1545,15 @@ export class StructuredSessionManager {
|
|
|
1472
1545
|
installHint: "请安装 @earendil-works/pi-coding-agent(或兼容的 Pi CLI),或重跑 `wand service:install` 刷新服务的 PATH",
|
|
1473
1546
|
});
|
|
1474
1547
|
}
|
|
1548
|
+
else if (provider === "gemini") {
|
|
1549
|
+
await this.runClaudeStreaming(id, updated, prompt, requestId, {
|
|
1550
|
+
runner: this.geminiRunner,
|
|
1551
|
+
provider: "gemini",
|
|
1552
|
+
commandLabel: "gemini -p --output-format stream-json",
|
|
1553
|
+
logKind: "gemini-json",
|
|
1554
|
+
installHint: "请安装 @google/gemini-cli ≥ 0.11(`npm i -g @google/gemini-cli@latest`),或重跑 `wand service:install` 刷新服务的 PATH",
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1475
1557
|
else if (runner === "claude-sdk") {
|
|
1476
1558
|
await this.runClaudeSdkStreaming(id, updated, prompt, requestId, skills);
|
|
1477
1559
|
}
|
|
@@ -1486,8 +1568,12 @@ export class StructuredSessionManager {
|
|
|
1486
1568
|
// Close handlers use this tagged error after they have already persisted
|
|
1487
1569
|
// the detailed failure. Re-throw even if an ended-event listener removed
|
|
1488
1570
|
// the session synchronously; there is no request-id marker to leak.
|
|
1489
|
-
if (error instanceof PersistedStructuredRunnerError)
|
|
1571
|
+
if (error instanceof PersistedStructuredRunnerError) {
|
|
1572
|
+
const retry = this.retryEmployeeCandidate(id, session, prompt, error, skills);
|
|
1573
|
+
if (retry)
|
|
1574
|
+
return await retry;
|
|
1490
1575
|
throw error;
|
|
1576
|
+
}
|
|
1491
1577
|
const current = this.sessions.get(id);
|
|
1492
1578
|
if (!current)
|
|
1493
1579
|
throw error;
|
|
@@ -1496,6 +1582,9 @@ export class StructuredSessionManager {
|
|
|
1496
1582
|
if (!this.isCurrentRequest(id, requestId)) {
|
|
1497
1583
|
return current;
|
|
1498
1584
|
}
|
|
1585
|
+
const retry = this.retryEmployeeCandidate(id, session, prompt, error, skills);
|
|
1586
|
+
if (retry)
|
|
1587
|
+
return await retry;
|
|
1499
1588
|
this.settlePendingPermission(id, undefined, structuredPermissionDenied("会话执行失败"));
|
|
1500
1589
|
this.turnApprovalMemory.delete(id);
|
|
1501
1590
|
const failed = {
|
|
@@ -2202,7 +2291,7 @@ export class StructuredSessionManager {
|
|
|
2202
2291
|
const hint = result.spawnError.code === "ENOENT"
|
|
2203
2292
|
? "(PATH 中找不到 codex 可执行文件;请确认 codex 已安装,或重跑 `wand service:install` 刷新服务的 PATH)"
|
|
2204
2293
|
: "";
|
|
2205
|
-
throw new
|
|
2294
|
+
throw new UnacceptedStructuredSpawnError(`codex exec 启动失败:${result.spawnError.message}${hint}`);
|
|
2206
2295
|
}
|
|
2207
2296
|
this.logger?.appendStructuredSpawn(sessionId, {
|
|
2208
2297
|
kind: "codex-exec-close",
|
|
@@ -2348,7 +2437,7 @@ export class StructuredSessionManager {
|
|
|
2348
2437
|
const hint = result.spawnError.code === "ENOENT"
|
|
2349
2438
|
? "(PATH 中找不到 grok;请安装 Grok Build CLI,或重跑 `wand service:install` 刷新服务 PATH)"
|
|
2350
2439
|
: "";
|
|
2351
|
-
throw new
|
|
2440
|
+
throw new UnacceptedStructuredSpawnError(`grok 启动失败:${result.spawnError.message}${hint}`);
|
|
2352
2441
|
}
|
|
2353
2442
|
this.logger?.appendStructuredSpawn(sessionId, {
|
|
2354
2443
|
kind: "grok-headless-close",
|
|
@@ -2496,7 +2585,7 @@ export class StructuredSessionManager {
|
|
|
2496
2585
|
const hint = result.spawnError.code === "ENOENT"
|
|
2497
2586
|
? "(PATH 中找不到 opencode;请安装 opencode-ai,或重跑 `wand service:install` 刷新服务 PATH)"
|
|
2498
2587
|
: "";
|
|
2499
|
-
throw new
|
|
2588
|
+
throw new UnacceptedStructuredSpawnError(`opencode run 启动失败:${result.spawnError.message}${hint}`);
|
|
2500
2589
|
}
|
|
2501
2590
|
this.logger?.appendStructuredSpawn(sessionId, {
|
|
2502
2591
|
kind: "opencode-run-close",
|
|
@@ -2673,9 +2762,9 @@ export class StructuredSessionManager {
|
|
|
2673
2762
|
flushEmit();
|
|
2674
2763
|
if (result.spawnError) {
|
|
2675
2764
|
const hint = result.spawnError.code === "ENOENT"
|
|
2676
|
-
? `(PATH 中找不到 ${provider === "qoder" ? "qodercli" : provider === "pi" ? "pi" : "claude"} 可执行文件;${options.installHint ?? "请确认 claude 已安装,或重跑 `wand service:install` 刷新服务的 PATH"})`
|
|
2765
|
+
? `(PATH 中找不到 ${provider === "qoder" ? "qodercli" : provider === "gemini" ? "gemini" : provider === "pi" ? "pi" : "claude"} 可执行文件;${options.installHint ?? "请确认 claude 已安装,或重跑 `wand service:install` 刷新服务的 PATH"})`
|
|
2677
2766
|
: "";
|
|
2678
|
-
throw new
|
|
2767
|
+
throw new UnacceptedStructuredSpawnError(`${commandLabel} 启动失败:${result.spawnError.message}${hint}`);
|
|
2679
2768
|
}
|
|
2680
2769
|
this.logger?.appendStructuredSpawn(sessionId, {
|
|
2681
2770
|
kind: `${logKind}-close`,
|
|
@@ -2706,6 +2795,9 @@ export class StructuredSessionManager {
|
|
|
2706
2795
|
if (provider === "pi" && isMissingPiSession(result.stderr, current.claudeSessionId)) {
|
|
2707
2796
|
failed.claudeSessionId = null;
|
|
2708
2797
|
}
|
|
2798
|
+
if (provider === "gemini" && isMissingGeminiSession(`${result.stderr}\n${errorText}`)) {
|
|
2799
|
+
failed.claudeSessionId = null;
|
|
2800
|
+
}
|
|
2709
2801
|
this.sessions.set(sessionId, failed);
|
|
2710
2802
|
this.saveAuthoritativeSession(failed);
|
|
2711
2803
|
this.emitStructuredSnapshot(failed);
|
package/dist/system-ai.js
CHANGED
|
@@ -260,7 +260,7 @@ export function discoverCliSystemAiConfigs(preferred, home = os.homedir()) {
|
|
|
260
260
|
const found = [];
|
|
261
261
|
const seen = new Set();
|
|
262
262
|
for (const provider of [...new Set(order)]) {
|
|
263
|
-
if (provider === "qoder" || provider === "pi")
|
|
263
|
+
if (provider === "qoder" || provider === "pi" || provider === "gemini")
|
|
264
264
|
continue;
|
|
265
265
|
let discovered;
|
|
266
266
|
try {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SYSTEM_EMPLOYEE_ID, SYSTEM_EMPLOYEE_KEY, SYSTEM_EMPLOYEE_NAME, SYSTEM_EMPLOYEE_TAG, isSystemSiliconEmployee, type SiliconEmployee } from "./ai-team-types.js";
|
|
2
|
+
import { type WandTaskAgent } from "./task-types.js";
|
|
3
|
+
import type { AiCliCandidate, SessionProvider } from "./types.js";
|
|
4
|
+
export { SYSTEM_EMPLOYEE_ID, SYSTEM_EMPLOYEE_KEY, SYSTEM_EMPLOYEE_NAME, SYSTEM_EMPLOYEE_TAG, isSystemSiliconEmployee, };
|
|
5
|
+
/**
|
|
6
|
+
* 内置的「系统运维」员工。
|
|
7
|
+
*
|
|
8
|
+
* Wand 自己的 AI 调用(commit message / tag、提示词优化、会话与任务标题、
|
|
9
|
+
* 员工起草)都由这位员工执行:名字、职责与人设写死在代码里,不可编辑、不可归档、
|
|
10
|
+
* 不可删除;只有执行候选(CLI 工具 + 模型 + 思考深度,按顺序降级)由用户维护。
|
|
11
|
+
*/
|
|
12
|
+
export declare const SYSTEM_EMPLOYEE_DUTY = "Wand \u7CFB\u7EDF\u8FD0\u7EF4\uFF1A\u670D\u52A1\u4E0E CLI \u7EBF\u8DEF\u3001\u66F4\u65B0\u5206\u53D1\u3001\u4ED3\u5E93\u4E0E\u4F1A\u8BDD\u64CD\u4F5C\uFF0CWand \u81EA\u6709 AI \u4EFB\u52A1\u7684\u6267\u884C\u8005\u3002";
|
|
13
|
+
/**
|
|
14
|
+
* 同时承担两个角色,所以规则写在一起:
|
|
15
|
+
* - 作为会话系统提示:用户直接找它做运维/功能实现;
|
|
16
|
+
* - 作为内部 AI 调用的系统提示:任务通常带严格输出格式。
|
|
17
|
+
*/
|
|
18
|
+
export declare const SYSTEM_EMPLOYEE_PROMPT: string;
|
|
19
|
+
export interface SystemEmployeeSeed {
|
|
20
|
+
/** 老配置里的「系统 AI 专用工具」;没有时跟随默认 provider。 */
|
|
21
|
+
cli?: SessionProvider | null;
|
|
22
|
+
/** 该工具下的模型;空 / "default" 表示跟随 provider 默认模型。 */
|
|
23
|
+
model?: string | null;
|
|
24
|
+
/** 没有专用工具时的默认 provider。 */
|
|
25
|
+
provider?: SessionProvider | null;
|
|
26
|
+
}
|
|
27
|
+
/** 首次创建内置员工时使用的候选:优先沿用用户已有的系统 AI 工具与模型。 */
|
|
28
|
+
export declare function systemEmployeeSeedAgents(seed?: SystemEmployeeSeed): WandTaskAgent[];
|
|
29
|
+
export declare function systemEmployeeDefinition(agents: WandTaskAgent[], now: string, existing?: SiliconEmployee | null): SiliconEmployee;
|
|
30
|
+
/** 内置员工只保留结构化候选(会话与内部 AI 调用都要求结构化执行)。 */
|
|
31
|
+
export declare function systemEmployeeAgentsOrNull(value: unknown): WandTaskAgent[] | null;
|
|
32
|
+
/**
|
|
33
|
+
* 内部 AI 调用的 CLI 降级链:按用户设置的顺序,模型为空 / "default" 时跟随 provider 默认值。
|
|
34
|
+
*/
|
|
35
|
+
export declare function systemEmployeeCliCandidates(employee: Pick<SiliconEmployee, "agents"> | null | undefined): AiCliCandidate[];
|