@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.
Files changed (58) hide show
  1. package/README.md +18 -6
  2. package/dist/agent-dispatch.d.ts +3 -0
  3. package/dist/agent-dispatch.js +5 -0
  4. package/dist/ai-team-runner.js +4 -2
  5. package/dist/ai-team-types.d.ts +34 -0
  6. package/dist/ai-team-types.js +12 -1
  7. package/dist/build-info.json +3 -3
  8. package/dist/config.d.ts +4 -3
  9. package/dist/config.js +25 -0
  10. package/dist/git-quick-commit.d.ts +9 -8
  11. package/dist/git-quick-commit.js +123 -8
  12. package/dist/models.d.ts +3 -0
  13. package/dist/models.js +24 -2
  14. package/dist/process-manager.js +16 -3
  15. package/dist/provider-cli-updater.d.ts +1 -1
  16. package/dist/provider-cli-updater.js +12 -1
  17. package/dist/server-ai-team-routes.js +4 -1
  18. package/dist/server-employee-routes.d.ts +22 -0
  19. package/dist/server-employee-routes.js +277 -0
  20. package/dist/server-session-routes.js +38 -13
  21. package/dist/server-settings-routes.js +5 -0
  22. package/dist/server-task-routes.d.ts +2 -0
  23. package/dist/server-task-routes.js +174 -22
  24. package/dist/server-update-routes.js +1 -1
  25. package/dist/server-workspace-routes.js +3 -0
  26. package/dist/server.js +16 -3
  27. package/dist/session-ai-context.d.ts +7 -2
  28. package/dist/session-ai-context.js +36 -9
  29. package/dist/session-provider.d.ts +6 -1
  30. package/dist/session-provider.js +27 -1
  31. package/dist/session-transport.js +4 -0
  32. package/dist/silicon-employee-dispatch.d.ts +9 -0
  33. package/dist/silicon-employee-dispatch.js +13 -0
  34. package/dist/silicon-employee-draft.d.ts +36 -0
  35. package/dist/silicon-employee-draft.js +146 -0
  36. package/dist/storage.d.ts +19 -2
  37. package/dist/storage.js +158 -7
  38. package/dist/structured-gemini-adapter.d.ts +40 -0
  39. package/dist/structured-gemini-adapter.js +198 -0
  40. package/dist/structured-provider-common.d.ts +2 -1
  41. package/dist/structured-provider-common.js +7 -2
  42. package/dist/structured-session-manager.d.ts +10 -0
  43. package/dist/structured-session-manager.js +106 -14
  44. package/dist/system-ai.js +1 -1
  45. package/dist/system-employee.d.ts +35 -0
  46. package/dist/system-employee.js +81 -0
  47. package/dist/task-types.d.ts +8 -1
  48. package/dist/task-types.js +7 -0
  49. package/dist/types.d.ts +23 -4
  50. package/dist/web-ui/content/ai-teams.js +13 -13
  51. package/dist/web-ui/content/scripts.js +64 -64
  52. package/dist/web-ui/content/styles.css +1 -1
  53. package/dist/web-ui/content/tailwind.css +3 -0
  54. package/dist/web-ui/embedded-assets.js +3 -3
  55. package/dist/web-ui/provider-identity.d.ts +2 -1
  56. package/dist/web-ui/provider-identity.js +10 -1
  57. package/dist/web-ui/session-activity.js +1 -1
  58. 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
- return runner === "pi-cli-json";
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
- : applyPiEvent(state, event);
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
- : runner === "claude-sdk"
1187
- ? "claude-agent-sdk (stream-json)"
1188
- : "claude -p --output-format stream-json",
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 Error(`codex exec 启动失败:${result.spawnError.message}${hint}`);
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 Error(`grok 启动失败:${result.spawnError.message}${hint}`);
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 Error(`opencode run 启动失败:${result.spawnError.message}${hint}`);
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 Error(`${commandLabel} 启动失败:${result.spawnError.message}${hint}`);
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[];