@yeaft/webchat-agent 0.1.485 → 0.1.487

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 (42) hide show
  1. package/package.json +1 -1
  2. package/unify/cli.js +5 -28
  3. package/unify/engine.js +8 -20
  4. package/unify/eval/cases/tool-use.js +1 -22
  5. package/unify/router/intent-classifier.js +444 -0
  6. package/unify/session.js +7 -0
  7. package/unify/skills.js +15 -9
  8. package/unify/tools/agent.js +0 -1
  9. package/unify/tools/apply-patch.js +0 -1
  10. package/unify/tools/ask-user.js +0 -1
  11. package/unify/tools/bash.js +0 -1
  12. package/unify/tools/close-agent.js +0 -1
  13. package/unify/tools/enter-worktree.js +0 -1
  14. package/unify/tools/exit-worktree.js +0 -1
  15. package/unify/tools/file-edit.js +0 -1
  16. package/unify/tools/file-read.js +0 -1
  17. package/unify/tools/file-write.js +0 -1
  18. package/unify/tools/glob.js +0 -1
  19. package/unify/tools/grep.js +0 -1
  20. package/unify/tools/history-search.js +0 -1
  21. package/unify/tools/image-generation.js +0 -1
  22. package/unify/tools/js-repl.js +0 -2
  23. package/unify/tools/list-agents.js +0 -1
  24. package/unify/tools/list-dir.js +0 -1
  25. package/unify/tools/mcp-tools.js +0 -2
  26. package/unify/tools/memory-query.js +0 -1
  27. package/unify/tools/memory-read.js +0 -1
  28. package/unify/tools/memory-search.js +0 -1
  29. package/unify/tools/memory-write.js +0 -1
  30. package/unify/tools/notebook-edit.js +0 -1
  31. package/unify/tools/request-permissions.js +0 -1
  32. package/unify/tools/send-message.js +0 -1
  33. package/unify/tools/skill.js +0 -1
  34. package/unify/tools/task-tools.js +0 -8
  35. package/unify/tools/thread-tools.js +0 -7
  36. package/unify/tools/tool-search.js +47 -56
  37. package/unify/tools/types.js +3 -6
  38. package/unify/tools/view-image.js +0 -1
  39. package/unify/tools/wait-agent.js +0 -1
  40. package/unify/tools/web-fetch.js +0 -1
  41. package/unify/tools/web-search.js +0 -1
  42. package/unify/tools/write-stdin.js +0 -1
@@ -0,0 +1,444 @@
1
+ /**
2
+ * intent-classifier.js — task-309 (Phase 2 Router).
3
+ *
4
+ * Routes an incoming user message to one of four intents relative to the
5
+ * current set of live threads + pending tasks:
6
+ *
7
+ * - 'continue' — append to currentThreadId (default / most common)
8
+ * - 'interrupt' — steal focus on another LIVE thread (e.g. user replies
9
+ * while another thread is mid-stream)
10
+ * - 'fork' — spawn a NEW thread from the current one
11
+ * - 'switch' — re-focus on a different existing thread
12
+ *
13
+ * ### Routing pipeline
14
+ *
15
+ * 1. **Explicit signal parse** (no LLM):
16
+ * - Prefix `@thread-<id>` → switch/interrupt that thread (direct).
17
+ * - Prefix `@task-<nnn>` → switch to the thread attached to that task,
18
+ * if any; otherwise fall through to LLM.
19
+ * 2. **User override lookup**: if UI previously called `.override(msgId,…)`
20
+ * for this message, return that decision verbatim.
21
+ * 3. **LLM classification**: one call to `primaryModel` (Q2 — router also
22
+ * uses primary; fast-model route disabled for this phase) with a small
23
+ * JSON-only prompt. Parse `{action, targetThreadId, reason}`.
24
+ * 4. **Fallback**: on ANY exception, unknown action, or unknown
25
+ * targetThreadId → degrade to `continue` on the current thread and
26
+ * record a `router.failure` trace event.
27
+ *
28
+ * ### Out of scope (task-310)
29
+ *
30
+ * - user_input_queue storage of pending messages.
31
+ * - Actual dispatch to an EngineInstance (the router just decides WHERE;
32
+ * task-310 owns the WHO/WHEN).
33
+ * - Concurrent stream flush-back semantics.
34
+ *
35
+ * This module only exposes `classify()` + `override()`; the caller owns the
36
+ * registry routing after the decision is returned.
37
+ */
38
+
39
+ /**
40
+ * @typedef {'continue'|'interrupt'|'fork'|'switch'} RouterAction
41
+ *
42
+ * @typedef {Object} RouterDecision
43
+ * @property {RouterAction} action
44
+ * @property {string} targetThreadId — resolved thread (always defined; for
45
+ * 'fork' this is the PARENT thread, the actual new-thread id is chosen
46
+ * by the caller when it creates the thread)
47
+ * @property {string} reason — short human-readable explanation
48
+ * @property {'explicit'|'override'|'llm'|'fallback'} [source]
49
+ *
50
+ * @typedef {Object} ThreadSummary — minimum info the classifier needs
51
+ * @property {string} id
52
+ * @property {string} [name]
53
+ * @property {string} [goal]
54
+ * @property {string} [status]
55
+ *
56
+ * @typedef {Object} PendingTask
57
+ * @property {string} id
58
+ * @property {string} [title]
59
+ * @property {string} [threadId] — attached thread, if any
60
+ * @property {string} [status]
61
+ *
62
+ * @typedef {Object} ClassifyInput
63
+ * @property {string} userMessage
64
+ * @property {string} currentThreadId
65
+ * @property {Array<ThreadSummary>} [allThreads]
66
+ * @property {Array<PendingTask>} [pendingTasks]
67
+ * @property {string} [messageId] — if provided, any stored override for this
68
+ * id is consulted before the LLM path
69
+ */
70
+
71
+ const VALID_ACTIONS = ['continue', 'interrupt', 'fork', 'switch'];
72
+
73
+ /**
74
+ * Match a leading `@thread-xxx` marker. Captures the thread id WITHOUT the
75
+ * `@` prefix. Case-sensitive (thread ids are canonical).
76
+ * Example matches: "@thread-main ...", "@thread-abcd1234 ..."
77
+ */
78
+ const THREAD_PREFIX_RE = /^@(thread-[A-Za-z0-9_-]+)\b\s*/;
79
+
80
+ /**
81
+ * Match a leading `@task-NNN` marker. Captures the task id WITHOUT the `@`.
82
+ * Example matches: "@task-309 ...", "@task-abc ..."
83
+ */
84
+ const TASK_PREFIX_RE = /^@(task-[A-Za-z0-9_-]+)\b\s*/;
85
+
86
+ export class IntentClassifier {
87
+ /** @type {object} */ #adapter;
88
+ /** @type {object} */ #trace;
89
+ /** @type {object} */ #config;
90
+ /** @type {Map<string, RouterDecision>} */ #overrides;
91
+
92
+ /**
93
+ * @param {{
94
+ * adapter: object,
95
+ * trace?: object,
96
+ * config: object,
97
+ * }} deps
98
+ */
99
+ constructor({ adapter, trace, config } = {}) {
100
+ if (!adapter || typeof adapter.stream !== 'function') {
101
+ throw new Error('IntentClassifier: adapter with .stream() is required');
102
+ }
103
+ if (!config || typeof config !== 'object') {
104
+ throw new Error('IntentClassifier: config is required');
105
+ }
106
+ this.#adapter = adapter;
107
+ this.#trace = trace || null;
108
+ this.#config = config;
109
+ this.#overrides = new Map();
110
+ }
111
+
112
+ /**
113
+ * Store a user correction for a specific messageId. Next `.classify()`
114
+ * call with the matching `messageId` will return this decision verbatim
115
+ * (and consume it, so a second call re-enters normal routing).
116
+ *
117
+ * Used by the UI "不对,我是问 X" affordance.
118
+ *
119
+ * @param {string} messageId
120
+ * @param {{ action: RouterAction, targetThreadId: string, reason?: string }} decision
121
+ * @returns {void}
122
+ */
123
+ override(messageId, decision) {
124
+ if (!messageId || typeof messageId !== 'string') {
125
+ throw new Error('IntentClassifier.override: messageId required');
126
+ }
127
+ if (!decision || !VALID_ACTIONS.includes(decision.action)) {
128
+ throw new Error(`IntentClassifier.override: invalid action ${decision && decision.action}`);
129
+ }
130
+ if (!decision.targetThreadId || typeof decision.targetThreadId !== 'string') {
131
+ throw new Error('IntentClassifier.override: targetThreadId required');
132
+ }
133
+ this.#overrides.set(messageId, {
134
+ action: decision.action,
135
+ targetThreadId: decision.targetThreadId,
136
+ reason: decision.reason || 'user_override',
137
+ source: 'override',
138
+ });
139
+ }
140
+
141
+ /** Whether an override is currently stored for a given messageId. */
142
+ hasOverride(messageId) {
143
+ return this.#overrides.has(messageId);
144
+ }
145
+
146
+ /** Test-only / admin — drop all stored overrides. */
147
+ clearOverrides() {
148
+ this.#overrides.clear();
149
+ }
150
+
151
+ /**
152
+ * Classify the user message into a router decision.
153
+ *
154
+ * Resolution order: override → explicit @prefix → LLM → fallback.
155
+ *
156
+ * @param {ClassifyInput} input
157
+ * @returns {Promise<RouterDecision>}
158
+ */
159
+ async classify(input) {
160
+ const {
161
+ userMessage,
162
+ currentThreadId,
163
+ allThreads = [],
164
+ pendingTasks = [],
165
+ messageId,
166
+ } = input || {};
167
+
168
+ if (!userMessage || typeof userMessage !== 'string') {
169
+ throw new Error('classify: userMessage is required');
170
+ }
171
+ if (!currentThreadId || typeof currentThreadId !== 'string') {
172
+ throw new Error('classify: currentThreadId is required');
173
+ }
174
+
175
+ // 1. User override takes precedence over everything else.
176
+ if (messageId && this.#overrides.has(messageId)) {
177
+ const decision = this.#overrides.get(messageId);
178
+ this.#overrides.delete(messageId);
179
+ return { ...decision, source: 'override' };
180
+ }
181
+
182
+ // 2. Explicit signals (no LLM call).
183
+ const explicit = this.#parseExplicit(userMessage, {
184
+ currentThreadId, allThreads, pendingTasks,
185
+ });
186
+ if (explicit) return explicit;
187
+
188
+ // 3. LLM classification (best-effort).
189
+ try {
190
+ const decision = await this.#classifyWithLLM({
191
+ userMessage, currentThreadId, allThreads, pendingTasks,
192
+ });
193
+ return this.#validateOrFallback(decision, {
194
+ currentThreadId, allThreads, reason: 'llm',
195
+ });
196
+ } catch (err) {
197
+ this.#traceFailure(err, { userMessage, currentThreadId });
198
+ return this.#fallback(currentThreadId, `classifier_exception: ${err.message}`);
199
+ }
200
+ }
201
+
202
+ // ──────────────────────────────────────────────────────────────
203
+ // Explicit-signal parser
204
+ // ──────────────────────────────────────────────────────────────
205
+
206
+ /**
207
+ * @param {string} msg
208
+ * @param {{ currentThreadId: string, allThreads: Array<ThreadSummary>, pendingTasks: Array<PendingTask> }} ctx
209
+ * @returns {RouterDecision|null}
210
+ */
211
+ #parseExplicit(msg, { currentThreadId, allThreads, pendingTasks }) {
212
+ const trimmed = msg.replace(/^\s+/, '');
213
+
214
+ // @thread-xxx
215
+ const tm = trimmed.match(THREAD_PREFIX_RE);
216
+ if (tm) {
217
+ const targetId = tm[1];
218
+ const known = allThreads.some(t => t && t.id === targetId);
219
+ if (!known) {
220
+ // Unknown thread — silently degrade. Record trace so ops can see it.
221
+ this.#traceFailure(
222
+ new Error(`unknown thread in @prefix: ${targetId}`),
223
+ { userMessage: msg, currentThreadId },
224
+ );
225
+ return this.#fallback(currentThreadId, `unknown_thread:${targetId}`);
226
+ }
227
+ const action = targetId === currentThreadId ? 'continue' : 'switch';
228
+ return {
229
+ action,
230
+ targetThreadId: targetId,
231
+ reason: `explicit @${targetId}`,
232
+ source: 'explicit',
233
+ };
234
+ }
235
+
236
+ // @task-NNN
237
+ const mt = trimmed.match(TASK_PREFIX_RE);
238
+ if (mt) {
239
+ const taskId = mt[1];
240
+ const task = pendingTasks.find(t => t && t.id === taskId);
241
+ if (task && task.threadId) {
242
+ const action = task.threadId === currentThreadId ? 'continue' : 'switch';
243
+ return {
244
+ action,
245
+ targetThreadId: task.threadId,
246
+ reason: `explicit @${taskId} → ${task.threadId}`,
247
+ source: 'explicit',
248
+ };
249
+ }
250
+ // Task unknown or not attached — fall through to LLM.
251
+ return null;
252
+ }
253
+
254
+ return null;
255
+ }
256
+
257
+ // ──────────────────────────────────────────────────────────────
258
+ // LLM classification path
259
+ // ──────────────────────────────────────────────────────────────
260
+
261
+ /** Build the prompt/messages for the router LLM call. */
262
+ #buildMessages({ userMessage, currentThreadId, allThreads, pendingTasks }) {
263
+ const system = [
264
+ 'You are a thread-routing classifier for a multi-thread AI chat.',
265
+ 'Given the user message and current thread context, pick exactly one action:',
266
+ " - 'continue' — the message belongs to the current thread",
267
+ " - 'interrupt' — it answers / redirects a DIFFERENT live thread",
268
+ " - 'fork' — it starts a new tangent that should be its own thread",
269
+ " - 'switch' — it explicitly re-focuses on another existing thread",
270
+ '',
271
+ 'Respond with ONE LINE of JSON, nothing else:',
272
+ '{"action":"<action>","targetThreadId":"<id>","reason":"<short>"}',
273
+ '',
274
+ 'Rules:',
275
+ '- For fork, set targetThreadId to the CURRENT thread (it is the parent).',
276
+ '- For continue, set targetThreadId to the CURRENT thread.',
277
+ '- For switch/interrupt, targetThreadId MUST be one of the known thread ids.',
278
+ '- If uncertain, pick continue.',
279
+ ].join('\n');
280
+
281
+ const ctx = {
282
+ currentThreadId,
283
+ threads: (allThreads || []).map(t => ({
284
+ id: t.id,
285
+ name: t.name || '',
286
+ goal: t.goal || '',
287
+ status: t.status || 'active',
288
+ })),
289
+ pendingTasks: (pendingTasks || []).map(t => ({
290
+ id: t.id,
291
+ title: t.title || '',
292
+ threadId: t.threadId || null,
293
+ })),
294
+ };
295
+
296
+ const user = [
297
+ 'Context:',
298
+ JSON.stringify(ctx),
299
+ '',
300
+ 'User message:',
301
+ userMessage,
302
+ ].join('\n');
303
+
304
+ return { system, messages: [{ role: 'user', content: user }] };
305
+ }
306
+
307
+ /** @returns {Promise<RouterDecision>} */
308
+ async #classifyWithLLM({ userMessage, currentThreadId, allThreads, pendingTasks }) {
309
+ const { system, messages } = this.#buildMessages({
310
+ userMessage, currentThreadId, allThreads, pendingTasks,
311
+ });
312
+ // Q2: router uses primaryModel (no fast-model split yet).
313
+ const model = this.#config.primaryModel || this.#config.model;
314
+ if (!model) {
315
+ throw new Error('router: no primaryModel configured');
316
+ }
317
+
318
+ let text = '';
319
+ for await (const event of this.#adapter.stream({
320
+ model,
321
+ system,
322
+ messages,
323
+ maxTokens: 256,
324
+ })) {
325
+ if (event && event.type === 'text_delta' && typeof event.text === 'string') {
326
+ text += event.text;
327
+ } else if (event && event.type === 'error') {
328
+ throw event.error || new Error('router stream error');
329
+ }
330
+ }
331
+ return parseLLMDecision(text);
332
+ }
333
+
334
+ // ──────────────────────────────────────────────────────────────
335
+ // Validation & fallback
336
+ // ──────────────────────────────────────────────────────────────
337
+
338
+ #validateOrFallback(decision, { currentThreadId, allThreads, reason }) {
339
+ if (!decision || !VALID_ACTIONS.includes(decision.action)) {
340
+ this.#traceFailure(
341
+ new Error(`invalid action from classifier: ${decision && decision.action}`),
342
+ { currentThreadId },
343
+ );
344
+ return this.#fallback(currentThreadId, 'invalid_action');
345
+ }
346
+ const known = new Set((allThreads || []).map(t => t && t.id).filter(Boolean));
347
+ known.add(currentThreadId);
348
+
349
+ // For fork/continue the target MUST be the current thread parent (we
350
+ // allow any known thread since callers may want to fork from a
351
+ // non-current parent, but continue MUST land on current).
352
+ if (decision.action === 'continue' && decision.targetThreadId !== currentThreadId) {
353
+ decision.targetThreadId = currentThreadId;
354
+ }
355
+ if (!decision.targetThreadId || !known.has(decision.targetThreadId)) {
356
+ this.#traceFailure(
357
+ new Error(`unknown targetThreadId: ${decision.targetThreadId}`),
358
+ { currentThreadId },
359
+ );
360
+ return this.#fallback(currentThreadId, 'unknown_target');
361
+ }
362
+ return {
363
+ action: decision.action,
364
+ targetThreadId: decision.targetThreadId,
365
+ reason: decision.reason || reason,
366
+ source: 'llm',
367
+ };
368
+ }
369
+
370
+ /** @returns {RouterDecision} */
371
+ #fallback(currentThreadId, reason) {
372
+ return {
373
+ action: 'continue',
374
+ targetThreadId: currentThreadId,
375
+ reason,
376
+ source: 'fallback',
377
+ };
378
+ }
379
+
380
+ #traceFailure(err, ctx) {
381
+ if (!this.#trace || typeof this.#trace.logEvent !== 'function') return;
382
+ try {
383
+ this.#trace.logEvent({
384
+ traceId: 'router',
385
+ eventType: 'router.failure',
386
+ eventData: {
387
+ error: err && err.message ? err.message : String(err),
388
+ currentThreadId: ctx && ctx.currentThreadId,
389
+ userMessage: ctx && typeof ctx.userMessage === 'string'
390
+ ? ctx.userMessage.slice(0, 200)
391
+ : undefined,
392
+ },
393
+ });
394
+ } catch {
395
+ // Trace must never propagate errors into the router path.
396
+ }
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Parse the LLM's single-line JSON response. Tolerates a leading/trailing
402
+ * code fence ```json ... ``` because some proxies wrap.
403
+ *
404
+ * @param {string} raw
405
+ * @returns {RouterDecision}
406
+ */
407
+ export function parseLLMDecision(raw) {
408
+ if (!raw || typeof raw !== 'string') {
409
+ throw new Error('empty classifier response');
410
+ }
411
+ let text = raw.trim();
412
+ // Strip ```json fences if present.
413
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
414
+ if (fenced) text = fenced[1].trim();
415
+ // Take the first { ... } block.
416
+ const start = text.indexOf('{');
417
+ const end = text.lastIndexOf('}');
418
+ if (start < 0 || end <= start) {
419
+ throw new Error('no JSON object in classifier response');
420
+ }
421
+ const slice = text.slice(start, end + 1);
422
+ let obj;
423
+ try {
424
+ obj = JSON.parse(slice);
425
+ } catch (e) {
426
+ throw new Error(`classifier response not valid JSON: ${e.message}`);
427
+ }
428
+ return {
429
+ action: obj.action,
430
+ targetThreadId: obj.targetThreadId,
431
+ reason: obj.reason || '',
432
+ };
433
+ }
434
+
435
+ /**
436
+ * Build an IntentClassifier from session-level deps. This is the entry
437
+ * point used by session.js to populate `session.router`.
438
+ *
439
+ * @param {{ adapter: object, trace?: object, config: object }} deps
440
+ * @returns {IntentClassifier}
441
+ */
442
+ export function createIntentClassifier(deps) {
443
+ return new IntentClassifier(deps);
444
+ }
package/unify/session.js CHANGED
@@ -27,6 +27,7 @@ import { initThreadStore } from './threads/store.js';
27
27
  import { Engine } from './engine.js';
28
28
  import { createThreadEngineRegistry } from './threads/engine-registry.js';
29
29
  import { MAIN_THREAD_ID } from './threads/store.js';
30
+ import { createIntentClassifier } from './router/intent-classifier.js';
30
31
  import { join } from 'path';
31
32
 
32
33
  /**
@@ -187,6 +188,11 @@ export async function loadSession(options = {}) {
187
188
  // Seed the main-thread instance so listActive() is non-empty from T=0.
188
189
  engineRegistry.ensure(MAIN_THREAD_ID);
189
190
 
191
+ // task-309 Phase 2 router: intent classifier that routes incoming user
192
+ // messages to the right EngineInstance. Shares the same adapter/trace/
193
+ // config as the engines so it can use primaryModel for classification.
194
+ const router = createIntentClassifier({ adapter, trace, config });
195
+
190
196
  // ─── 10. Build session ─────────────────────────────────
191
197
  const status = {
192
198
  skills: skillManager.size,
@@ -217,6 +223,7 @@ export async function loadSession(options = {}) {
217
223
  return {
218
224
  engine,
219
225
  engineRegistry,
226
+ router,
220
227
  adapter,
221
228
  config,
222
229
  conversationStore,
package/unify/skills.js CHANGED
@@ -381,17 +381,21 @@ export class SkillManager {
381
381
  }
382
382
 
383
383
  /**
384
- * List all skills (metadata only — no content), optionally filtered by mode.
384
+ * List all skills (metadata only — no content).
385
385
  * This is the "progressive disclosure" list tier.
386
386
  *
387
- * @param {string} [mode] 'chat' | 'work' | undefined (all)
387
+ * task-311: the legacy `mode` parameter (chat/work filter) is accepted but
388
+ * ignored — Unify no longer has mode distinction, so every skill is treated
389
+ * as universally applicable. The `mode` field on each record is still
390
+ * surfaced for historic YAML compatibility.
391
+ *
392
+ * @param {string} [_mode] — deprecated, ignored
388
393
  * @returns {Array<{ name: string, description: string, trigger: string, mode: string, category?: string, platforms?: string[], keywords?: string[], source: string, hasReferences: boolean, hasTemplates: boolean }>}
389
394
  */
390
- list(mode) {
395
+ list(_mode) {
391
396
  const skills = [...this.#skills.values()];
392
- const filtered = mode ? skills.filter(s => s.mode === 'both' || s.mode === mode) : skills;
393
397
 
394
- return filtered.map(s => ({
398
+ return skills.map(s => ({
395
399
  name: s.name,
396
400
  description: s.description || '',
397
401
  trigger: s.trigger || '',
@@ -447,20 +451,22 @@ export class SkillManager {
447
451
  * Find skills relevant to a prompt.
448
452
  * Enhanced matching: regex triggers, keyword lists, name/description match.
449
453
  *
454
+ * task-311: the `mode` parameter is accepted but ignored (all skills are
455
+ * considered universally applicable since mode distinction was removed).
456
+ *
450
457
  * @param {string} prompt — user's prompt
451
- * @param {string} [mode] — filter by mode
458
+ * @param {string} [_mode] — deprecated, ignored
452
459
  * @returns {Skill[]}
453
460
  */
454
- findRelevant(prompt, mode) {
461
+ findRelevant(prompt, _mode) {
455
462
  if (!prompt) return [];
456
463
 
457
464
  const lowerPrompt = prompt.toLowerCase();
458
465
  const cleanPrompt = lowerPrompt.replace(/[^\w\s]/g, '');
459
466
  const promptWords = cleanPrompt.split(/\s+/).filter(w => w.length > 2);
460
467
  const allSkills = [...this.#skills.values()];
461
- const filtered = mode ? allSkills.filter(s => s.mode === 'both' || s.mode === mode) : allSkills;
462
468
 
463
- return filtered.filter(skill => {
469
+ return allSkills.filter(skill => {
464
470
  // 1. Regex or keyword trigger match
465
471
  if (skill.trigger && matchTrigger(skill.trigger, lowerPrompt, promptWords)) {
466
472
  return true;
@@ -229,7 +229,6 @@ Guidelines:
229
229
  },
230
230
  required: ['name'],
231
231
  },
232
- modes: ['work'],
233
232
  isConcurrencySafe: () => false,
234
233
  isReadOnly: () => false,
235
234
  async execute(input, ctx) {
@@ -115,7 +115,6 @@ Guidelines:
115
115
  },
116
116
  required: ['patch'],
117
117
  },
118
- modes: ['chat', 'work'],
119
118
  isConcurrencySafe: () => false,
120
119
  isReadOnly: () => false,
121
120
  isDestructive: () => false,
@@ -38,7 +38,6 @@ Guidelines:
38
38
  },
39
39
  required: ['question'],
40
40
  },
41
- modes: ['chat', 'work'],
42
41
  isConcurrencySafe: () => false,
43
42
  isReadOnly: () => true,
44
43
  async execute(input, ctx) {
@@ -137,7 +137,6 @@ Guidelines:
137
137
  },
138
138
  required: ['command'],
139
139
  },
140
- modes: ['chat', 'work'],
141
140
  isConcurrencySafe: () => false,
142
141
  isReadOnly: () => false,
143
142
  isDestructive: (input) => {
@@ -25,7 +25,6 @@ The agent's result (if any) is returned before closing.`,
25
25
  },
26
26
  required: ['agent_id'],
27
27
  },
28
- modes: ['work'],
29
28
  isConcurrencySafe: () => false,
30
29
  isReadOnly: () => false,
31
30
  async execute(input, ctx) {
@@ -39,7 +39,6 @@ Returns the worktree path and branch name.`,
39
39
  },
40
40
  },
41
41
  },
42
- modes: ['chat', 'work'],
43
42
  isDestructive: () => false,
44
43
  async execute(input, ctx) {
45
44
  const cwd = ctx?.cwd || process.cwd();
@@ -41,7 +41,6 @@ unless discard_changes is set to true.`,
41
41
  },
42
42
  required: ['path', 'action'],
43
43
  },
44
- modes: ['chat', 'work'],
45
44
  isDestructive: (input) => input?.action === 'remove',
46
45
  async execute(input, ctx) {
47
46
  const worktreePath = resolve(input.path);
@@ -46,7 +46,6 @@ Guidelines:
46
46
  },
47
47
  required: ['file_path', 'old_string', 'new_string'],
48
48
  },
49
- modes: ['chat', 'work'],
50
49
  isConcurrencySafe: () => false,
51
50
  isReadOnly: () => false,
52
51
  isDestructive: () => false,
@@ -60,7 +60,6 @@ Guidelines:
60
60
  },
61
61
  required: ['file_path'],
62
62
  },
63
- modes: ['chat', 'work'],
64
63
  isConcurrencySafe: () => true,
65
64
  isReadOnly: () => true,
66
65
  async execute(input, ctx) {
@@ -34,7 +34,6 @@ Guidelines:
34
34
  },
35
35
  required: ['file_path', 'content'],
36
36
  },
37
- modes: ['chat', 'work'],
38
37
  isConcurrencySafe: () => false,
39
38
  isReadOnly: () => false,
40
39
  isDestructive: () => false,
@@ -95,7 +95,6 @@ Guidelines:
95
95
  },
96
96
  required: ['pattern'],
97
97
  },
98
- modes: ['chat', 'work'],
99
98
  isConcurrencySafe: () => true,
100
99
  isReadOnly: () => true,
101
100
  async execute(input, ctx) {
@@ -207,7 +207,6 @@ Guidelines:
207
207
  },
208
208
  required: ['pattern'],
209
209
  },
210
- modes: ['chat', 'work'],
211
210
  isConcurrencySafe: () => true,
212
211
  isReadOnly: () => true,
213
212
  async execute(input, ctx) {
@@ -30,7 +30,6 @@ Results are returned newest-first with message role and content.`,
30
30
  },
31
31
  required: ['keyword'],
32
32
  },
33
- modes: ['chat', 'work'],
34
33
  isConcurrencySafe: () => true,
35
34
  isReadOnly: () => true,
36
35
  async execute(input, ctx) {
@@ -36,7 +36,6 @@ Guidelines:
36
36
  },
37
37
  required: ['prompt'],
38
38
  },
39
- modes: ['chat', 'work'],
40
39
  isConcurrencySafe: () => true,
41
40
  isReadOnly: () => false,
42
41
  async execute(input, ctx) {
@@ -69,7 +69,6 @@ Guidelines:
69
69
  },
70
70
  required: ['code'],
71
71
  },
72
- modes: ['chat', 'work'],
73
72
  isConcurrencySafe: () => false,
74
73
  isReadOnly: () => true,
75
74
  async execute(input, ctx) {
@@ -112,7 +111,6 @@ Use when you want a clean slate.`,
112
111
  type: 'object',
113
112
  properties: {},
114
113
  },
115
- modes: ['chat', 'work'],
116
114
  isConcurrencySafe: () => false,
117
115
  isReadOnly: () => false,
118
116
  async execute(input, ctx) {
@@ -20,7 +20,6 @@ and message counts. Use to monitor parallel task progress.`,
20
20
  },
21
21
  },
22
22
  },
23
- modes: ['work'],
24
23
  isConcurrencySafe: () => true,
25
24
  isReadOnly: () => true,
26
25
  async execute(input, ctx) {