@agent-spaces/server 0.1.2 → 0.2.1
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/dist/adapters/claude-code-runtime/index.js +4 -1
- package/dist/adapters/git.js +2 -21
- package/dist/agents/issue-agent-runner.js +35 -21
- package/dist/agents/issue-task-controller.js +120 -73
- package/dist/agents/planner-agent.js +9 -12
- package/dist/app.js +10 -0
- package/dist/hooks/agent-hooks.js +5 -8
- package/dist/package.json +1 -1
- package/dist/routes/agent.js +40 -17
- package/dist/routes/channel.js +4 -1
- package/dist/routes/command.js +108 -0
- package/dist/routes/folder.js +44 -7
- package/dist/routes/issue.js +57 -23
- package/dist/routes/mcp.js +50 -0
- package/dist/routes/skill.js +57 -0
- package/dist/routes/subscription.js +49 -0
- package/dist/routes/task.js +16 -1
- package/dist/routes/workflow.js +63 -0
- package/dist/routes/workspace.js +2 -21
- package/dist/services/agent.js +140 -76
- package/dist/services/builtin-tools.js +72 -0
- package/dist/services/channel.js +5 -5
- package/dist/services/command-process-manager.js +136 -0
- package/dist/services/command.js +48 -0
- package/dist/services/issue.js +5 -6
- package/dist/services/mcp.js +120 -0
- package/dist/services/notification-hub/bot-commands.js +48 -16
- package/dist/services/notification-hub/helpers.js +12 -0
- package/dist/services/pty.js +10 -3
- package/dist/services/skill.js +171 -0
- package/dist/services/subscription/aicode.js +45 -0
- package/dist/services/subscription/base.js +3 -0
- package/dist/services/subscription/index.js +20 -0
- package/dist/services/subscription/minimax.js +60 -0
- package/dist/services/subscription/zhipu.js +39 -0
- package/dist/services/workflow.js +203 -0
- package/dist/services/workspace.js +0 -1
- package/dist/storage/command-store.js +17 -0
- package/dist/storage/subscription-store.js +44 -0
- package/dist/storage/workflow-store.js +40 -0
- package/dist/web/404.html +1 -1
- package/dist/web/__next.__PAGE__.txt +4 -4
- package/dist/web/__next._full.txt +22 -21
- package/dist/web/__next._head.txt +4 -4
- package/dist/web/__next._index.txt +9 -8
- package/dist/web/__next._tree.txt +2 -2
- package/dist/web/_next/static/MpJepsgfsHGGcc7rwFU8Y/_buildManifest.js +11 -0
- package/dist/web/_next/static/chunks/0-245kun-watp.js +2 -0
- package/dist/web/_next/static/chunks/0-ca_fo-yp3~z.js +1 -0
- package/dist/web/_next/static/chunks/0-vgd3j-zh3m8.js +2 -0
- package/dist/web/_next/static/chunks/0.4.g.8yf4rs0.js +3 -0
- package/dist/web/_next/static/chunks/02m_-ngl9w8co.js +1 -0
- package/dist/web/_next/static/chunks/03jbh7ud0jw~g.js +1 -0
- package/dist/web/_next/static/chunks/07kqxmubf5dua.js +1 -0
- package/dist/web/_next/static/chunks/088q-5_51dsrw.js +1 -0
- package/dist/web/_next/static/chunks/09_ki3dc5cfwv.css +1 -0
- package/dist/web/_next/static/chunks/09jryrjps0vl2.js +1 -0
- package/dist/web/_next/static/chunks/0b2tump5duj9j.js +1 -0
- package/dist/web/_next/static/chunks/0dctkv_2d2r0g.js +8 -0
- package/dist/web/_next/static/chunks/0efgv37kxyht8.js +1 -0
- package/dist/web/_next/static/chunks/0fwvdy-ml8wpk.js +1 -0
- package/dist/web/_next/static/chunks/0g4vm6.v0o_lt.js +1 -0
- package/dist/web/_next/static/chunks/0g_b4t~000.o~.js +179 -0
- package/dist/web/_next/static/chunks/0gyede80jpy3n.js +4 -0
- package/dist/web/_next/static/chunks/0h256h-tu4e0r.js +2 -0
- package/dist/web/_next/static/chunks/0ib18ul605e~a.js +1 -0
- package/dist/web/_next/static/chunks/0j1g_rd9t5ot1.js +1 -0
- package/dist/web/_next/static/chunks/0jn~llaqcxo4q.js +1 -0
- package/dist/web/_next/static/chunks/0jvmviuftg5e2.css +1 -0
- package/dist/web/_next/static/chunks/0lb8l2~6s_owo.js +1 -0
- package/dist/web/_next/static/chunks/0ni5ot2603_28.js +1 -0
- package/dist/web/_next/static/chunks/0pep4mkvt3.rh.js +31 -0
- package/dist/web/_next/static/chunks/0pq2670_ezbcj.js +1 -0
- package/dist/web/_next/static/chunks/0q_scqkk9.t53.js +2 -0
- package/dist/web/_next/static/chunks/0rrdur.v1a5r7.js +1 -0
- package/dist/web/_next/static/chunks/0spo.tmfeas-o.js +1 -0
- package/dist/web/_next/static/chunks/0tta_56qj1z8-.js +1 -0
- package/dist/web/_next/static/chunks/0u88ij9dqqh~-.js +1 -0
- package/dist/web/_next/static/chunks/0zl19l5tuoppw.js +1 -0
- package/dist/web/_next/static/chunks/11n16hogah-5..js +1 -0
- package/dist/web/_next/static/chunks/1250wo~-5dgyx.js +1 -0
- package/dist/web/_next/static/chunks/14n8i2xz4_y-e.js +1 -0
- package/dist/web/_next/static/chunks/160ji-.dfvm20.js +1 -0
- package/dist/web/_next/static/chunks/turbopack-0lxiiw.jhevml.js +1 -0
- package/dist/web/_next/static/media/favicon.0~ekuj.zhggpa.ico +0 -0
- package/dist/web/_not-found/__next._full.txt +24 -19
- package/dist/web/_not-found/__next._head.txt +4 -4
- package/dist/web/_not-found/__next._index.txt +9 -8
- package/dist/web/_not-found/__next._not-found.__PAGE__.txt +2 -2
- package/dist/web/_not-found/__next._not-found.txt +3 -3
- package/dist/web/_not-found/__next._tree.txt +2 -2
- package/dist/web/_not-found.html +1 -1
- package/dist/web/_not-found.txt +24 -19
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/index.html +1 -1
- package/dist/web/index.txt +22 -21
- package/dist/web/login/__next._full.txt +24 -23
- package/dist/web/login/__next._head.txt +4 -4
- package/dist/web/login/__next._index.txt +9 -8
- package/dist/web/login/__next._tree.txt +2 -2
- package/dist/web/login/__next.login.__PAGE__.txt +4 -4
- package/dist/web/login/__next.login.txt +3 -3
- package/dist/web/login.html +1 -1
- package/dist/web/login.txt +24 -23
- package/dist/web/settings/__next._full.txt +34 -0
- package/dist/web/settings/__next._head.txt +6 -0
- package/dist/web/settings/__next._index.txt +11 -0
- package/dist/web/settings/__next._tree.txt +8 -0
- package/dist/web/settings/__next.settings.__PAGE__.txt +9 -0
- package/dist/web/settings/__next.settings.txt +7 -0
- package/dist/web/settings/agents/__next._full.txt +38 -0
- package/dist/web/settings/agents/__next._head.txt +6 -0
- package/dist/web/settings/agents/__next._index.txt +11 -0
- package/dist/web/settings/agents/__next._tree.txt +8 -0
- package/dist/web/settings/agents/__next.settings.agents.__PAGE__.txt +9 -0
- package/dist/web/settings/agents/__next.settings.agents.txt +5 -0
- package/dist/web/settings/agents/__next.settings.txt +7 -0
- package/dist/web/settings/agents.html +1 -0
- package/dist/web/settings/agents.txt +38 -0
- package/dist/web/settings/mcps/__next._full.txt +38 -0
- package/dist/web/settings/mcps/__next._head.txt +6 -0
- package/dist/web/settings/mcps/__next._index.txt +11 -0
- package/dist/web/settings/mcps/__next._tree.txt +8 -0
- package/dist/web/settings/mcps/__next.settings.mcps.__PAGE__.txt +9 -0
- package/dist/web/settings/mcps/__next.settings.mcps.txt +5 -0
- package/dist/web/settings/mcps/__next.settings.txt +7 -0
- package/dist/web/settings/mcps.html +1 -0
- package/dist/web/settings/mcps.txt +38 -0
- package/dist/web/settings/models/__next._full.txt +38 -0
- package/dist/web/settings/models/__next._head.txt +6 -0
- package/dist/web/settings/models/__next._index.txt +11 -0
- package/dist/web/settings/models/__next._tree.txt +8 -0
- package/dist/web/settings/models/__next.settings.models.__PAGE__.txt +9 -0
- package/dist/web/settings/models/__next.settings.models.txt +5 -0
- package/dist/web/settings/models/__next.settings.txt +7 -0
- package/dist/web/settings/models.html +1 -0
- package/dist/web/settings/models.txt +38 -0
- package/dist/web/settings/providers/__next._full.txt +38 -0
- package/dist/web/settings/providers/__next._head.txt +6 -0
- package/dist/web/settings/providers/__next._index.txt +11 -0
- package/dist/web/settings/providers/__next._tree.txt +8 -0
- package/dist/web/settings/providers/__next.settings.providers.__PAGE__.txt +9 -0
- package/dist/web/settings/providers/__next.settings.providers.txt +5 -0
- package/dist/web/settings/providers/__next.settings.txt +7 -0
- package/dist/web/settings/providers.html +1 -0
- package/dist/web/settings/providers.txt +38 -0
- package/dist/web/settings/skills/__next._full.txt +38 -0
- package/dist/web/settings/skills/__next._head.txt +6 -0
- package/dist/web/settings/skills/__next._index.txt +11 -0
- package/dist/web/settings/skills/__next._tree.txt +8 -0
- package/dist/web/settings/skills/__next.settings.skills.__PAGE__.txt +9 -0
- package/dist/web/settings/skills/__next.settings.skills.txt +5 -0
- package/dist/web/settings/skills/__next.settings.txt +7 -0
- package/dist/web/settings/skills.html +1 -0
- package/dist/web/settings/skills.txt +38 -0
- package/dist/web/settings.html +1 -0
- package/dist/web/settings.txt +34 -0
- package/dist/web/workflows/__next._full.txt +35 -0
- package/dist/web/workflows/__next._head.txt +6 -0
- package/dist/web/workflows/__next._index.txt +11 -0
- package/dist/web/workflows/__next._tree.txt +9 -0
- package/dist/web/workflows/__next.workflows.__PAGE__.txt +10 -0
- package/dist/web/workflows/__next.workflows.txt +5 -0
- package/dist/web/workflows.html +1 -0
- package/dist/web/workflows.txt +35 -0
- package/dist/web/workspace/_/__next._full.txt +25 -20
- package/dist/web/workspace/_/__next._head.txt +4 -4
- package/dist/web/workspace/_/__next._index.txt +9 -8
- package/dist/web/workspace/_/__next._tree.txt +2 -2
- package/dist/web/workspace/_/__next.workspace.$d$id.__PAGE__.txt +3 -3
- package/dist/web/workspace/_/__next.workspace.$d$id.txt +3 -3
- package/dist/web/workspace/_/__next.workspace.txt +3 -3
- package/dist/web/workspace/_.html +1 -1
- package/dist/web/workspace/_.txt +25 -20
- package/dist/web/workspaces/__next._full.txt +24 -23
- package/dist/web/workspaces/__next._head.txt +4 -4
- package/dist/web/workspaces/__next._index.txt +9 -8
- package/dist/web/workspaces/__next._tree.txt +2 -2
- package/dist/web/workspaces/__next.workspaces.__PAGE__.txt +4 -4
- package/dist/web/workspaces/__next.workspaces.txt +3 -3
- package/dist/web/workspaces.html +1 -1
- package/dist/web/workspaces.txt +24 -23
- package/dist/ws/agent-prompt.js +84 -0
- package/dist/ws/agent-runner.js +592 -0
- package/dist/ws/handler.js +9 -1200
- package/dist/ws/html-utils.js +30 -0
- package/dist/ws/message-parts.js +498 -0
- package/package.json +3 -2
- package/dist/web/_next/static/chunks/038whpa0zpnas.js +0 -1
- package/dist/web/_next/static/chunks/06q5go~xoz5a3.js +0 -1
- package/dist/web/_next/static/chunks/095.wizobwtyg.js +0 -2
- package/dist/web/_next/static/chunks/0bfg.w~u-83h5.js +0 -1
- package/dist/web/_next/static/chunks/0ce7i6~sb20rv.js +0 -113
- package/dist/web/_next/static/chunks/0d4~pcva1uk-a.js +0 -1
- package/dist/web/_next/static/chunks/0iq70n_u75nyu.js +0 -1
- package/dist/web/_next/static/chunks/0memz-8zsbxpu.js +0 -1
- package/dist/web/_next/static/chunks/0nw~w.3~twebx.js +0 -2
- package/dist/web/_next/static/chunks/0pyytgz~5vt0f.js +0 -31
- package/dist/web/_next/static/chunks/0ujjcrz~1nq.q.css +0 -1
- package/dist/web/_next/static/chunks/0vdnx9n41dyjl.js +0 -1
- package/dist/web/_next/static/chunks/0yl4mqmxtll6g.js +0 -1
- package/dist/web/_next/static/chunks/0zcbfka5tcle3.js +0 -1
- package/dist/web/_next/static/chunks/0~eo58u99i.fr.js +0 -8
- package/dist/web/_next/static/chunks/13_2vxyccsv84.js +0 -4
- package/dist/web/_next/static/chunks/13wfs~urgxu0w.js +0 -1
- package/dist/web/_next/static/chunks/157~3c6wqkt83.js +0 -1
- package/dist/web/_next/static/chunks/15jvow_z4uq-..js +0 -1
- package/dist/web/_next/static/chunks/15v697nf~r-cy.js +0 -2
- package/dist/web/_next/static/chunks/turbopack-164~7ulq9o9yc.js +0 -1
- package/dist/web/_next/static/media/favicon.0x3dzn~oxb6tn.ico +0 -0
- package/dist/web/_next/static/owMnKmxATbtXK_J_k_uHh/_buildManifest.js +0 -21
- /package/dist/web/_next/static/{owMnKmxATbtXK_J_k_uHh → MpJepsgfsHGGcc7rwFU8Y}/_clientMiddlewareManifest.js +0 -0
- /package/dist/web/_next/static/{owMnKmxATbtXK_J_k_uHh → MpJepsgfsHGGcc7rwFU8Y}/_ssgManifest.js +0 -0
package/dist/ws/handler.js
CHANGED
|
@@ -1,46 +1,11 @@
|
|
|
1
1
|
import { addConnection, broadcastToWorkspace } from './connection-manager.js';
|
|
2
2
|
import { handleTerminalEvent } from './terminal-handler.js';
|
|
3
|
-
import { createMessage
|
|
4
|
-
import { getChannel
|
|
5
|
-
import * as issueService from '../services/issue.js';
|
|
6
|
-
import { createIssueFunctionTools } from '../services/builtin-tools.js';
|
|
7
|
-
import { startScheduler } from '../agents/scheduler-agent.js';
|
|
3
|
+
import { createMessage } from '../services/message.js';
|
|
4
|
+
import { getChannel } from '../services/channel.js';
|
|
8
5
|
import * as agentService from '../services/agent.js';
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import { createAgentRuntime } from '../adapters/agent-runtime.js';
|
|
12
|
-
import { saveToolDetails } from '../services/tool-detail.js';
|
|
13
|
-
import { readWorkspacePrompt } from '../services/workspace-prompt.js';
|
|
14
|
-
import { getThinkingRuntimeConfig } from '../services/llm-model-config.js';
|
|
6
|
+
import { runMentionedAgent, stopChannelRuns, handleAnswerQuestion, ensureScheduler, makeContext } from './agent-runner.js';
|
|
7
|
+
import { stripHtml, extractMentionIds } from './html-utils.js';
|
|
15
8
|
const handlers = new Map();
|
|
16
|
-
// Track which workspaces have schedulers running
|
|
17
|
-
const activeSchedulers = new Set();
|
|
18
|
-
const activeChannelRuns = new Map();
|
|
19
|
-
const pendingQuestionRuns = new Map();
|
|
20
|
-
function ensureScheduler(workspaceId, ctx) {
|
|
21
|
-
if (!activeSchedulers.has(workspaceId)) {
|
|
22
|
-
activeSchedulers.add(workspaceId);
|
|
23
|
-
setTimeout(() => {
|
|
24
|
-
startScheduler(workspaceId, ctx);
|
|
25
|
-
console.log(`[ws] scheduler started for workspace ${workspaceId}`);
|
|
26
|
-
}, 10_000);
|
|
27
|
-
console.log(`[ws] scheduler scheduled for workspace ${workspaceId} in 10000ms`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function makeContext(workspaceId) {
|
|
31
|
-
return {
|
|
32
|
-
workspaceId,
|
|
33
|
-
broadcast: (event, data) => broadcastToWorkspace(workspaceId, event, data),
|
|
34
|
-
getSession: (sessionId) => agentService.getById(workspaceId, sessionId),
|
|
35
|
-
updateSessionStatus: (sessionId, status, extra) => agentService.updateStatus(workspaceId, sessionId, status, extra),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function getRuntimeBaseURL(provider, apiBase) {
|
|
39
|
-
if (provider === 'openai-responses-to-anthropic-messages'
|
|
40
|
-
|| provider === 'openai-chat-completions-to-anthropic-messages')
|
|
41
|
-
return undefined;
|
|
42
|
-
return apiBase;
|
|
43
|
-
}
|
|
44
9
|
export function registerHandler(event, handler) {
|
|
45
10
|
handlers.set(event, handler);
|
|
46
11
|
}
|
|
@@ -82,7 +47,7 @@ for (const evt of terminalEvents) {
|
|
|
82
47
|
handleTerminalEvent(ws, workspaceId, evt, data);
|
|
83
48
|
});
|
|
84
49
|
}
|
|
85
|
-
// Register channel
|
|
50
|
+
// Register channel handlers
|
|
86
51
|
registerHandler('channel.message', (_ws, workspaceId, data) => {
|
|
87
52
|
const { channelId, content, type, mentions, attachments } = data;
|
|
88
53
|
if (!channelId || (!content && !attachments?.length))
|
|
@@ -107,1169 +72,12 @@ registerHandler('channel.stop', (_ws, workspaceId, data) => {
|
|
|
107
72
|
return;
|
|
108
73
|
stopChannelRuns(workspaceId, channelId);
|
|
109
74
|
});
|
|
110
|
-
registerHandler('channel.answer_question',
|
|
111
|
-
const { channelId, messageId, questionId, answer } = data;
|
|
112
|
-
const trimmed = answer?.trim();
|
|
113
|
-
if (!channelId || !messageId || !questionId || !trimmed)
|
|
114
|
-
return;
|
|
115
|
-
const message = listMessages(workspaceId, channelId).find((item) => item.id === messageId);
|
|
116
|
-
if (!message)
|
|
117
|
-
return;
|
|
118
|
-
const updatedParts = message.parts?.map((part) => {
|
|
119
|
-
if (part.type !== 'ask_user_question' || part.id !== questionId)
|
|
120
|
-
return part;
|
|
121
|
-
return { ...part, status: 'answered', answer: trimmed };
|
|
122
|
-
});
|
|
123
|
-
const updated = updateMessage(workspaceId, channelId, messageId, {
|
|
124
|
-
status: 'streaming',
|
|
125
|
-
parts: updatedParts,
|
|
126
|
-
});
|
|
127
|
-
if (updated)
|
|
128
|
-
broadcastToWorkspace(workspaceId, 'channel.message.updated', updated);
|
|
129
|
-
const key = questionRunKey(workspaceId, channelId, messageId, questionId);
|
|
130
|
-
const pending = pendingQuestionRuns.get(key);
|
|
131
|
-
pendingQuestionRuns.delete(key);
|
|
132
|
-
if (!pending)
|
|
133
|
-
return;
|
|
134
|
-
const seedQuestion = questionFromAnsweredPart(updatedParts, questionId);
|
|
135
|
-
void runMentionedAgent(workspaceId, channelId, pending.agentConfigId, [
|
|
136
|
-
'The user answered your previous question.',
|
|
137
|
-
`Question: ${pending.question}`,
|
|
138
|
-
`Answer: ${trimmed}`,
|
|
139
|
-
'Continue from this answer and complete the original task.',
|
|
140
|
-
].join('\n'), {
|
|
141
|
-
messageId,
|
|
142
|
-
seedOutput: normalizeOutputLines([message.content]),
|
|
143
|
-
seedQuestions: seedQuestion ? [seedQuestion] : [],
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
async function runMentionedAgent(workspaceId, channelId, agentConfigId, prompt, options = {}) {
|
|
147
|
-
const preset = agentService.listPresets(workspaceId)?.find((agent) => agent.id === agentConfigId);
|
|
148
|
-
if (!preset || preset.enabled === false)
|
|
149
|
-
return;
|
|
150
|
-
const session = agentService.create(workspaceId, preset.role, preset.id);
|
|
151
|
-
broadcastToWorkspace(workspaceId, 'agent.started', session);
|
|
152
|
-
agentService.updateStatus(workspaceId, session.id, 'active');
|
|
153
|
-
broadcastToWorkspace(workspaceId, 'agent.status_changed', {
|
|
154
|
-
agentId: session.id,
|
|
155
|
-
from: 'idle',
|
|
156
|
-
to: 'active',
|
|
157
|
-
});
|
|
158
|
-
const configDir = agentService.getAgentConfigDir(workspaceId, preset);
|
|
159
|
-
const mcpServers = agentService.getMcpServers(preset.mcps);
|
|
160
|
-
const skills = agentService.getAvailableSkillNames(configDir, preset.skills);
|
|
161
|
-
const workspace = wsService.getById(workspaceId);
|
|
162
|
-
const { channel, issue } = resolveIssueChannelContext(workspaceId, channelId);
|
|
163
|
-
const functionTools = createIssueFunctionTools(workspaceId, channel, {
|
|
164
|
-
senderId: preset.id,
|
|
165
|
-
senderRole: preset.role,
|
|
166
|
-
}, preset.tools);
|
|
167
|
-
const startTime = Date.now();
|
|
168
|
-
const existingMessage = options.messageId
|
|
169
|
-
? listMessages(workspaceId, channelId).find((message) => message.id === options.messageId)
|
|
170
|
-
: undefined;
|
|
171
|
-
const pending = existingMessage
|
|
172
|
-
? updateMessage(workspaceId, channelId, existingMessage.id, {
|
|
173
|
-
status: 'streaming',
|
|
174
|
-
metadata: {
|
|
175
|
-
...existingMessage.metadata,
|
|
176
|
-
agentSessionId: session.id,
|
|
177
|
-
runtime: preset.runtimeKind,
|
|
178
|
-
model: preset.modelId,
|
|
179
|
-
duration: 0,
|
|
180
|
-
},
|
|
181
|
-
}) ?? existingMessage
|
|
182
|
-
: createMessage(workspaceId, channelId, {
|
|
183
|
-
senderId: preset.id,
|
|
184
|
-
senderRole: preset.role,
|
|
185
|
-
content: 'Agent is processing...',
|
|
186
|
-
type: 'text',
|
|
187
|
-
status: 'streaming',
|
|
188
|
-
metadata: {
|
|
189
|
-
agentSessionId: session.id,
|
|
190
|
-
runtime: preset.runtimeKind,
|
|
191
|
-
model: preset.modelId,
|
|
192
|
-
duration: 0,
|
|
193
|
-
},
|
|
194
|
-
parts: [
|
|
195
|
-
{
|
|
196
|
-
id: `reasoning-${session.id}`,
|
|
197
|
-
type: 'reasoning',
|
|
198
|
-
text: 'Preparing agent runtime and loading conversation context...',
|
|
199
|
-
status: 'streaming',
|
|
200
|
-
},
|
|
201
|
-
],
|
|
202
|
-
});
|
|
203
|
-
broadcastToWorkspace(workspaceId, existingMessage ? 'channel.message.updated' : 'channel.message', pending);
|
|
204
|
-
let activeRun;
|
|
205
|
-
const liveReasoning = [];
|
|
206
|
-
try {
|
|
207
|
-
const runtime = createAgentRuntime({
|
|
208
|
-
kind: preset.runtimeKind,
|
|
209
|
-
provider: preset.modelProvider,
|
|
210
|
-
model: preset.modelId,
|
|
211
|
-
apiKey: preset.apiKey,
|
|
212
|
-
baseURL: getRuntimeBaseURL(preset.modelProvider, preset.apiBase),
|
|
213
|
-
adapterBaseURL: preset.apiBase,
|
|
214
|
-
...getThinkingRuntimeConfig(preset),
|
|
215
|
-
});
|
|
216
|
-
activeRun = {
|
|
217
|
-
agentId: session.id,
|
|
218
|
-
agentConfigId,
|
|
219
|
-
messageId: pending.id,
|
|
220
|
-
runtime,
|
|
221
|
-
};
|
|
222
|
-
trackChannelRun(workspaceId, channelId, activeRun);
|
|
223
|
-
const history = listMessages(workspaceId, channelId, { limit: 20 });
|
|
224
|
-
const liveOutput = [...(options.seedOutput ?? [])];
|
|
225
|
-
const askUserQuestions = [...(options.seedQuestions ?? [])];
|
|
226
|
-
const toolDetails = new Map();
|
|
227
|
-
const toolUseDetailIds = new Map();
|
|
228
|
-
let lastLiveUpdate = 0;
|
|
229
|
-
const broadcastLiveParts = (force = false) => {
|
|
230
|
-
const now = Date.now();
|
|
231
|
-
if (!force && now - lastLiveUpdate < 120)
|
|
232
|
-
return;
|
|
233
|
-
lastLiveUpdate = now;
|
|
234
|
-
const parts = buildAgentMessageParts({
|
|
235
|
-
sessionId: session.id,
|
|
236
|
-
workspaceRoot: workspace?.boundDirs?.[0],
|
|
237
|
-
presetName: preset.name || preset.role,
|
|
238
|
-
role: preset.role,
|
|
239
|
-
model: preset.modelId,
|
|
240
|
-
systemPrompt: preset.systemPrompt,
|
|
241
|
-
mcpServers: Object.keys(mcpServers ?? {}),
|
|
242
|
-
skills,
|
|
243
|
-
builtInTools: buildBuiltInTools(functionTools, channel, issue),
|
|
244
|
-
output: liveOutput,
|
|
245
|
-
reasoning: liveReasoning,
|
|
246
|
-
toolDetails,
|
|
247
|
-
askUserQuestions,
|
|
248
|
-
success: true,
|
|
249
|
-
});
|
|
250
|
-
if (parts.length === 0)
|
|
251
|
-
return;
|
|
252
|
-
const status = askUserQuestions.some((question) => !question.answer) ? 'waiting_for_user' : 'streaming';
|
|
253
|
-
const live = updateMessage(workspaceId, channelId, pending.id, {
|
|
254
|
-
content: liveOutput.join('\n') || pending.content,
|
|
255
|
-
status,
|
|
256
|
-
parts,
|
|
257
|
-
metadata: {
|
|
258
|
-
...pending.metadata,
|
|
259
|
-
duration: Date.now() - startTime,
|
|
260
|
-
},
|
|
261
|
-
});
|
|
262
|
-
if (live)
|
|
263
|
-
broadcastToWorkspace(workspaceId, 'channel.message.updated', live);
|
|
264
|
-
};
|
|
265
|
-
const result = await runtime.execute(buildAgentPrompt(workspaceId, preset.systemPrompt, prompt, history, {
|
|
266
|
-
mcpServers: Object.keys(mcpServers ?? {}),
|
|
267
|
-
skills,
|
|
268
|
-
boundDirs: workspace?.boundDirs,
|
|
269
|
-
builtInTools: buildBuiltInTools(functionTools, channel, issue),
|
|
270
|
-
}), agentService.resolveWorkingDir(workspaceId, preset), {
|
|
271
|
-
maxTurns: 100,
|
|
272
|
-
functionTools,
|
|
273
|
-
mcpServers,
|
|
274
|
-
skills,
|
|
275
|
-
configDir,
|
|
276
|
-
sandboxDirs: preset.sandboxDirs,
|
|
277
|
-
onEvent: (event) => {
|
|
278
|
-
if (event.type === 'reasoning') {
|
|
279
|
-
liveReasoning.push({ text: event.text, status: event.status });
|
|
280
|
-
broadcastToWorkspace(workspaceId, 'agent.output', { agentId: session.id, data: event.text });
|
|
281
|
-
broadcastLiveParts();
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
if (event.type === 'tool_use') {
|
|
285
|
-
if (event.name === 'AskUserQuestion') {
|
|
286
|
-
const question = parseAskUserQuestion(event.id, event.input);
|
|
287
|
-
askUserQuestions.push(question);
|
|
288
|
-
pendingQuestionRuns.set(questionRunKey(workspaceId, channelId, pending.id, question.id), {
|
|
289
|
-
agentConfigId,
|
|
290
|
-
question: question.question,
|
|
291
|
-
});
|
|
292
|
-
broadcastLiveParts(true);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
// Intercept TodoWrite to persist todos to channel
|
|
296
|
-
if (event.name === 'TodoWrite' && event.input && typeof event.input === 'object') {
|
|
297
|
-
const input = event.input;
|
|
298
|
-
if (Array.isArray(input.todos)) {
|
|
299
|
-
const todos = input.todos.map((t, index) => ({
|
|
300
|
-
id: String(t.id ?? t.activeForm ?? t.subject ?? t.title ?? t.content ?? index),
|
|
301
|
-
subject: String(t.subject ?? t.title ?? t.activeForm ?? t.content ?? ''),
|
|
302
|
-
description: t.description || t.content ? String(t.description ?? t.content) : undefined,
|
|
303
|
-
status: (['pending', 'in_progress', 'completed'].includes(t.status) ? t.status : 'pending'),
|
|
304
|
-
activeForm: t.activeForm ? String(t.activeForm) : undefined,
|
|
305
|
-
}));
|
|
306
|
-
const updated = updateChannel(workspaceId, channelId, { todos });
|
|
307
|
-
if (updated)
|
|
308
|
-
broadcastToWorkspace(workspaceId, 'channel.updated', updated);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
const detailId = buildToolDetailId(event.id, event.line);
|
|
312
|
-
toolUseDetailIds.set(event.id, detailId);
|
|
313
|
-
toolDetails.set(detailId, {
|
|
314
|
-
id: detailId,
|
|
315
|
-
workspaceId,
|
|
316
|
-
channelId,
|
|
317
|
-
messageId: pending.id,
|
|
318
|
-
title: summarizeToolLine(event.line, workspace?.boundDirs?.[0]).title,
|
|
319
|
-
raw: event.line,
|
|
320
|
-
input: event.input,
|
|
321
|
-
createdAt: new Date().toISOString(),
|
|
322
|
-
});
|
|
323
|
-
saveToolDetails(workspaceId, channelId, Array.from(toolDetails.values()));
|
|
324
|
-
liveOutput.push(event.line);
|
|
325
|
-
broadcastToWorkspace(workspaceId, 'agent.output', { agentId: session.id, data: event.line });
|
|
326
|
-
broadcastLiveParts();
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
if (event.type === 'tool_result') {
|
|
330
|
-
const askedQuestion = event.toolUseId
|
|
331
|
-
? askUserQuestions.find((question) => question.toolUseId === event.toolUseId)
|
|
332
|
-
: undefined;
|
|
333
|
-
if (askedQuestion)
|
|
334
|
-
return;
|
|
335
|
-
const detail = findToolDetailForResult(event.toolUseId, event.result, toolUseDetailIds, toolDetails, workspace?.boundDirs?.[0]);
|
|
336
|
-
if (detail && isAgentSpacesIssueTool(detail.raw || detail.title)) {
|
|
337
|
-
const updatedChannel = getChannel(workspaceId, channelId);
|
|
338
|
-
if (updatedChannel)
|
|
339
|
-
broadcastToWorkspace(workspaceId, 'channel.updated', updatedChannel);
|
|
340
|
-
const updatedIssue = updatedChannel?.issueId ? issueService.getById(workspaceId, updatedChannel.issueId) : null;
|
|
341
|
-
if (updatedIssue) {
|
|
342
|
-
broadcastToWorkspace(workspaceId, isCreateCurrentChannelIssueTool(detail.raw || detail.title) ? 'issue.created' : 'issue.updated', updatedIssue);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
if (detail) {
|
|
346
|
-
detail.output = event.result;
|
|
347
|
-
detail.updatedAt = new Date().toISOString();
|
|
348
|
-
saveToolDetails(workspaceId, channelId, [detail]);
|
|
349
|
-
}
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (event.type !== 'output')
|
|
353
|
-
return;
|
|
354
|
-
liveOutput.push(event.line);
|
|
355
|
-
broadcastToWorkspace(workspaceId, 'agent.output', { agentId: session.id, data: event.line });
|
|
356
|
-
broadcastLiveParts();
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
broadcastLiveParts(true);
|
|
360
|
-
if (activeRun.stopped)
|
|
361
|
-
return;
|
|
362
|
-
const displayOutput = mergeRuntimeOutput(liveOutput, result.output);
|
|
363
|
-
if (shouldWaitForUserAnswer(askUserQuestions, result.summary, result.error, displayOutput)) {
|
|
364
|
-
const waitingOutput = stripAskUserQuestionErrorLines(liveOutput);
|
|
365
|
-
const waiting = updateMessage(workspaceId, channelId, pending.id, {
|
|
366
|
-
content: waitingOutput.join('\n') || pending.content,
|
|
367
|
-
type: 'text',
|
|
368
|
-
status: 'waiting_for_user',
|
|
369
|
-
metadata: {
|
|
370
|
-
agentSessionId: session.id,
|
|
371
|
-
runtime: preset.runtimeKind,
|
|
372
|
-
model: preset.modelId,
|
|
373
|
-
summary: 'Waiting for user answer',
|
|
374
|
-
duration: Date.now() - startTime,
|
|
375
|
-
},
|
|
376
|
-
parts: buildAgentMessageParts({
|
|
377
|
-
sessionId: session.id,
|
|
378
|
-
workspaceRoot: workspace?.boundDirs?.[0],
|
|
379
|
-
presetName: preset.name || preset.role,
|
|
380
|
-
role: preset.role,
|
|
381
|
-
model: preset.modelId,
|
|
382
|
-
systemPrompt: preset.systemPrompt,
|
|
383
|
-
mcpServers: Object.keys(mcpServers ?? {}),
|
|
384
|
-
skills,
|
|
385
|
-
output: waitingOutput,
|
|
386
|
-
reasoning: liveReasoning,
|
|
387
|
-
toolDetails,
|
|
388
|
-
askUserQuestions,
|
|
389
|
-
success: true,
|
|
390
|
-
}),
|
|
391
|
-
});
|
|
392
|
-
if (waiting)
|
|
393
|
-
broadcastToWorkspace(workspaceId, 'channel.message.updated', waiting);
|
|
394
|
-
agentService.updateStatus(workspaceId, session.id, 'blocked');
|
|
395
|
-
broadcastToWorkspace(workspaceId, 'agent.status_changed', {
|
|
396
|
-
agentId: session.id,
|
|
397
|
-
from: 'active',
|
|
398
|
-
to: 'blocked',
|
|
399
|
-
});
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
if (liveOutput.length === 0) {
|
|
403
|
-
for (const line of result.output) {
|
|
404
|
-
broadcastToWorkspace(workspaceId, 'agent.output', { agentId: session.id, data: line });
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
agentService.complete(workspaceId, session.id, result.success ? undefined : result.error, {
|
|
408
|
-
runtime: preset.runtimeKind,
|
|
409
|
-
model: preset.modelId,
|
|
410
|
-
summary: result.summary,
|
|
411
|
-
output: displayOutput,
|
|
412
|
-
durationMs: Date.now() - startTime,
|
|
413
|
-
usage: result.usage,
|
|
414
|
-
costUsd: result.costUsd,
|
|
415
|
-
});
|
|
416
|
-
broadcastToWorkspace(workspaceId, 'agent.completed', {
|
|
417
|
-
agentId: session.id,
|
|
418
|
-
result: {
|
|
419
|
-
success: result.success,
|
|
420
|
-
summary: result.summary,
|
|
421
|
-
artifacts: result.artifacts,
|
|
422
|
-
error: result.error,
|
|
423
|
-
},
|
|
424
|
-
error: result.error,
|
|
425
|
-
});
|
|
426
|
-
const reply = updateMessage(workspaceId, channelId, pending.id, {
|
|
427
|
-
content: result.success ? displayOutput.join('\n') : result.error || displayOutput.join('\n'),
|
|
428
|
-
type: 'text',
|
|
429
|
-
status: result.success ? 'completed' : 'error',
|
|
430
|
-
metadata: {
|
|
431
|
-
agentSessionId: session.id,
|
|
432
|
-
runtime: preset.runtimeKind,
|
|
433
|
-
model: preset.modelId,
|
|
434
|
-
summary: result.summary,
|
|
435
|
-
duration: Date.now() - startTime,
|
|
436
|
-
},
|
|
437
|
-
parts: buildAgentMessageParts({
|
|
438
|
-
sessionId: session.id,
|
|
439
|
-
workspaceRoot: workspace?.boundDirs?.[0],
|
|
440
|
-
presetName: preset.name || preset.role,
|
|
441
|
-
role: preset.role,
|
|
442
|
-
model: preset.modelId,
|
|
443
|
-
usage: result.usage,
|
|
444
|
-
systemPrompt: preset.systemPrompt,
|
|
445
|
-
mcpServers: Object.keys(mcpServers ?? {}),
|
|
446
|
-
skills,
|
|
447
|
-
builtInTools: buildBuiltInTools(functionTools, channel, issue),
|
|
448
|
-
output: displayOutput,
|
|
449
|
-
reasoning: liveReasoning,
|
|
450
|
-
toolDetails,
|
|
451
|
-
askUserQuestions,
|
|
452
|
-
success: result.success,
|
|
453
|
-
error: result.error,
|
|
454
|
-
}),
|
|
455
|
-
});
|
|
456
|
-
if (reply)
|
|
457
|
-
broadcastToWorkspace(workspaceId, 'channel.message.updated', reply);
|
|
458
|
-
}
|
|
459
|
-
catch (err) {
|
|
460
|
-
if (activeRun?.stopped)
|
|
461
|
-
return;
|
|
462
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
463
|
-
agentService.complete(workspaceId, session.id, error, {
|
|
464
|
-
runtime: preset.runtimeKind,
|
|
465
|
-
model: preset.modelId,
|
|
466
|
-
summary: error,
|
|
467
|
-
output: [error],
|
|
468
|
-
durationMs: Date.now() - startTime,
|
|
469
|
-
});
|
|
470
|
-
broadcastToWorkspace(workspaceId, 'agent.error', { agentId: session.id, error });
|
|
471
|
-
const reply = updateMessage(workspaceId, channelId, pending.id, {
|
|
472
|
-
content: error,
|
|
473
|
-
type: 'text',
|
|
474
|
-
status: 'error',
|
|
475
|
-
metadata: {
|
|
476
|
-
agentSessionId: session.id,
|
|
477
|
-
runtime: preset.runtimeKind,
|
|
478
|
-
model: preset.modelId,
|
|
479
|
-
duration: Date.now() - startTime,
|
|
480
|
-
},
|
|
481
|
-
parts: buildAgentMessageParts({
|
|
482
|
-
sessionId: session.id,
|
|
483
|
-
workspaceRoot: workspace?.boundDirs?.[0],
|
|
484
|
-
presetName: preset.name || preset.role,
|
|
485
|
-
role: preset.role,
|
|
486
|
-
model: preset.modelId,
|
|
487
|
-
systemPrompt: preset.systemPrompt,
|
|
488
|
-
mcpServers: Object.keys(mcpServers ?? {}),
|
|
489
|
-
skills,
|
|
490
|
-
builtInTools: buildBuiltInTools(functionTools, channel, issue),
|
|
491
|
-
output: [error],
|
|
492
|
-
reasoning: liveReasoning,
|
|
493
|
-
toolDetails: new Map(),
|
|
494
|
-
askUserQuestions: [],
|
|
495
|
-
success: false,
|
|
496
|
-
error,
|
|
497
|
-
}),
|
|
498
|
-
});
|
|
499
|
-
if (reply)
|
|
500
|
-
broadcastToWorkspace(workspaceId, 'channel.message.updated', reply);
|
|
501
|
-
}
|
|
502
|
-
finally {
|
|
503
|
-
untrackChannelRun(workspaceId, channelId, session.id);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function resolveIssueChannelContext(workspaceId, channelId) {
|
|
507
|
-
let channel = getChannel(workspaceId, channelId);
|
|
508
|
-
let issue = channel?.issueId ? issueService.getById(workspaceId, channel.issueId) : null;
|
|
509
|
-
if (issue || channel?.type !== 'issue')
|
|
510
|
-
return { channel, issue };
|
|
511
|
-
issue = issueService.list(workspaceId).find((item) => item.channelId === channelId) ?? null;
|
|
512
|
-
channel = getChannel(workspaceId, channelId);
|
|
513
|
-
return { channel, issue };
|
|
514
|
-
}
|
|
515
|
-
function isAgentSpacesIssueTool(name) {
|
|
516
|
-
return Boolean(name && /(?:agent-spaces\.)?(?:CreateCurrentChannelIssue|ViewCurrentChannelIssue|AddCurrentChannelComment)/.test(name));
|
|
517
|
-
}
|
|
518
|
-
function isCreateCurrentChannelIssueTool(name) {
|
|
519
|
-
return Boolean(name && /(?:agent-spaces\.)?CreateCurrentChannelIssue/.test(name));
|
|
520
|
-
}
|
|
521
|
-
function channelRunKey(workspaceId, channelId) {
|
|
522
|
-
return `${workspaceId}:${channelId}`;
|
|
523
|
-
}
|
|
524
|
-
function trackChannelRun(workspaceId, channelId, run) {
|
|
525
|
-
const key = channelRunKey(workspaceId, channelId);
|
|
526
|
-
const runs = activeChannelRuns.get(key) ?? new Map();
|
|
527
|
-
runs.set(run.agentId, run);
|
|
528
|
-
activeChannelRuns.set(key, runs);
|
|
529
|
-
}
|
|
530
|
-
function untrackChannelRun(workspaceId, channelId, agentId) {
|
|
531
|
-
const key = channelRunKey(workspaceId, channelId);
|
|
532
|
-
const runs = activeChannelRuns.get(key);
|
|
533
|
-
if (!runs)
|
|
534
|
-
return;
|
|
535
|
-
runs.delete(agentId);
|
|
536
|
-
if (runs.size === 0)
|
|
537
|
-
activeChannelRuns.delete(key);
|
|
538
|
-
}
|
|
539
|
-
function stopChannelRuns(workspaceId, channelId) {
|
|
540
|
-
const runs = activeChannelRuns.get(channelRunKey(workspaceId, channelId));
|
|
541
|
-
if (!runs || runs.size === 0) {
|
|
542
|
-
markInactiveChannelRunsStopped(workspaceId, channelId);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
for (const run of runs.values()) {
|
|
546
|
-
run.stopped = true;
|
|
547
|
-
run.runtime.stop();
|
|
548
|
-
agentService.complete(workspaceId, run.agentId, 'Stopped by user');
|
|
549
|
-
broadcastToWorkspace(workspaceId, 'agent.status_changed', {
|
|
550
|
-
agentId: run.agentId,
|
|
551
|
-
from: 'active',
|
|
552
|
-
to: 'crashed',
|
|
553
|
-
});
|
|
554
|
-
broadcastToWorkspace(workspaceId, 'agent.error', {
|
|
555
|
-
agentId: run.agentId,
|
|
556
|
-
error: 'Stopped by user',
|
|
557
|
-
});
|
|
558
|
-
const message = updateMessage(workspaceId, channelId, run.messageId, {
|
|
559
|
-
content: 'Stopped by user',
|
|
560
|
-
status: 'error',
|
|
561
|
-
parts: [
|
|
562
|
-
{
|
|
563
|
-
id: `terminal-stopped-${run.agentId}`,
|
|
564
|
-
type: 'terminal',
|
|
565
|
-
output: 'Stopped by user',
|
|
566
|
-
status: 'error',
|
|
567
|
-
},
|
|
568
|
-
],
|
|
569
|
-
});
|
|
570
|
-
if (message)
|
|
571
|
-
broadcastToWorkspace(workspaceId, 'channel.message.updated', message);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
export function hasActiveChannelRuns(workspaceId, channelId) {
|
|
575
|
-
return Boolean(activeChannelRuns.get(channelRunKey(workspaceId, channelId))?.size);
|
|
576
|
-
}
|
|
577
|
-
export function markInactiveChannelRunsStopped(workspaceId, channelId) {
|
|
578
|
-
if (hasActiveChannelRuns(workspaceId, channelId))
|
|
579
|
-
return [];
|
|
580
|
-
const stopped = [];
|
|
581
|
-
for (const message of listMessages(workspaceId, channelId)) {
|
|
582
|
-
if (message.status !== 'pending' && message.status !== 'streaming')
|
|
583
|
-
continue;
|
|
584
|
-
const updated = updateMessage(workspaceId, channelId, message.id, {
|
|
585
|
-
content: message.content || 'Stopped by user',
|
|
586
|
-
status: 'error',
|
|
587
|
-
parts: message.parts?.map((part) => {
|
|
588
|
-
if ('status' in part && part.status === 'streaming') {
|
|
589
|
-
return { ...part, status: 'completed' };
|
|
590
|
-
}
|
|
591
|
-
return part;
|
|
592
|
-
}),
|
|
593
|
-
});
|
|
594
|
-
if (updated) {
|
|
595
|
-
stopped.push(updated);
|
|
596
|
-
broadcastToWorkspace(workspaceId, 'channel.message.updated', updated);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
return stopped;
|
|
600
|
-
}
|
|
601
|
-
function buildAgentMessageParts(input) {
|
|
602
|
-
const lines = normalizeOutputLines(input.output);
|
|
603
|
-
const finalTextRange = findFinalTextRange(lines);
|
|
604
|
-
const finalText = finalTextRange
|
|
605
|
-
? collapseRepeatedTextBlock(lines.slice(finalTextRange.start, finalTextRange.end + 1)).join('\n').trim()
|
|
606
|
-
: '';
|
|
607
|
-
const usage = input.usage ?? extractUsage(lines);
|
|
608
|
-
const parts = [];
|
|
609
|
-
const chainItems = buildChainItems(lines, finalTextRange, finalText, input.workspaceRoot, input.toolDetails);
|
|
610
|
-
const reasoningText = normalizeReasoningText(input.reasoning);
|
|
611
|
-
if (reasoningText) {
|
|
612
|
-
parts.push({
|
|
613
|
-
id: `reasoning-${input.sessionId}`,
|
|
614
|
-
type: 'reasoning',
|
|
615
|
-
text: reasoningText,
|
|
616
|
-
status: input.success ? 'completed' : 'streaming',
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
if (chainItems.length > 0) {
|
|
620
|
-
parts.push({
|
|
621
|
-
id: `chain-${input.sessionId}`,
|
|
622
|
-
type: 'chain',
|
|
623
|
-
chains: chainItems,
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
for (const subagent of extractSubagentBlocks(lines, input.sessionId, input.toolDetails)) {
|
|
627
|
-
parts.push(subagent);
|
|
628
|
-
}
|
|
629
|
-
for (const question of input.askUserQuestions ?? []) {
|
|
630
|
-
parts.push({
|
|
631
|
-
id: question.id,
|
|
632
|
-
type: 'ask_user_question',
|
|
633
|
-
question: question.question,
|
|
634
|
-
choices: question.choices,
|
|
635
|
-
status: question.answer ? 'answered' : 'requested',
|
|
636
|
-
answer: question.answer,
|
|
637
|
-
toolUseId: question.toolUseId,
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
if (hasTokenUsage(usage)) {
|
|
641
|
-
parts.push({
|
|
642
|
-
id: `context-${input.sessionId}`,
|
|
643
|
-
type: 'context',
|
|
644
|
-
usedTokens: getTotalTokens(usage),
|
|
645
|
-
maxTokens: 128_000,
|
|
646
|
-
modelId: input.model,
|
|
647
|
-
usage,
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
if (input.error) {
|
|
651
|
-
parts.push({
|
|
652
|
-
id: `terminal-error-${input.sessionId}`,
|
|
653
|
-
type: 'terminal',
|
|
654
|
-
output: input.error,
|
|
655
|
-
status: 'error',
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
if (finalText && finalText !== input.error) {
|
|
659
|
-
parts.push({
|
|
660
|
-
id: `text-${input.sessionId}`,
|
|
661
|
-
type: 'text',
|
|
662
|
-
text: finalText,
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
return parts;
|
|
666
|
-
}
|
|
667
|
-
function normalizeReasoningText(reasoning) {
|
|
668
|
-
if (!reasoning?.length)
|
|
669
|
-
return '';
|
|
670
|
-
return collapseRepeatedTextBlock(reasoning
|
|
671
|
-
.map((item) => item.text.trim())
|
|
672
|
-
.filter(Boolean)).join('\n\n').trim();
|
|
673
|
-
}
|
|
674
|
-
function hasTokenUsage(usage) {
|
|
675
|
-
return Boolean(usage.totalTokens || usage.inputTokens || usage.outputTokens || usage.cachedInputTokens || usage.reasoningTokens);
|
|
676
|
-
}
|
|
677
|
-
function getTotalTokens(usage) {
|
|
678
|
-
return usage.totalTokens ?? (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0) + (usage.cachedInputTokens ?? 0) + (usage.reasoningTokens ?? 0);
|
|
679
|
-
}
|
|
680
|
-
function normalizeOutputLines(output) {
|
|
681
|
-
const lines = output
|
|
682
|
-
.flatMap((line) => line.split(/\r?\n/))
|
|
683
|
-
.map((line) => line.trimEnd())
|
|
684
|
-
.filter((line) => line.trim());
|
|
685
|
-
const seenInitLines = new Set();
|
|
686
|
-
return lines.filter((line) => {
|
|
687
|
-
if (!/^Claude Code initialized\b/i.test(line))
|
|
688
|
-
return true;
|
|
689
|
-
if (seenInitLines.has(line))
|
|
690
|
-
return false;
|
|
691
|
-
seenInitLines.add(line);
|
|
692
|
-
return true;
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
function mergeRuntimeOutput(liveOutput, resultOutput) {
|
|
696
|
-
if (liveOutput.length === 0)
|
|
697
|
-
return resultOutput;
|
|
698
|
-
const finalResult = resultOutput.at(-1)?.trim();
|
|
699
|
-
if (!finalResult)
|
|
700
|
-
return liveOutput;
|
|
701
|
-
if (liveOutput.some((line) => normalizeMessageText(line) === normalizeMessageText(finalResult))) {
|
|
702
|
-
return liveOutput;
|
|
703
|
-
}
|
|
704
|
-
return [...liveOutput, finalResult];
|
|
705
|
-
}
|
|
706
|
-
function collapseRepeatedTextBlock(lines) {
|
|
707
|
-
let next = [...lines];
|
|
708
|
-
while (next.length > 1 && next.length % 2 === 0) {
|
|
709
|
-
const middle = next.length / 2;
|
|
710
|
-
const first = next.slice(0, middle);
|
|
711
|
-
const second = next.slice(middle);
|
|
712
|
-
if (!sameTextBlock(first, second))
|
|
713
|
-
break;
|
|
714
|
-
next = first;
|
|
715
|
-
}
|
|
716
|
-
return next;
|
|
717
|
-
}
|
|
718
|
-
function sameTextBlock(left, right) {
|
|
719
|
-
if (left.length !== right.length)
|
|
720
|
-
return false;
|
|
721
|
-
return normalizeMessageText(left.join('\n')) === normalizeMessageText(right.join('\n'));
|
|
722
|
-
}
|
|
723
|
-
function findFinalTextRange(lines) {
|
|
724
|
-
let end = -1;
|
|
725
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
726
|
-
if (isFinalAnswerLine(lines[index])) {
|
|
727
|
-
end = index;
|
|
728
|
-
break;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
if (end < 0)
|
|
732
|
-
return null;
|
|
733
|
-
let start = end;
|
|
734
|
-
for (let index = end - 1; index >= 0; index -= 1) {
|
|
735
|
-
if (!isFinalAnswerLine(lines[index]))
|
|
736
|
-
break;
|
|
737
|
-
start = index;
|
|
738
|
-
}
|
|
739
|
-
return { start, end };
|
|
740
|
-
}
|
|
741
|
-
function buildChainItems(lines, finalTextRange, finalText, workspaceRoot, toolDetails) {
|
|
742
|
-
let toolIndex = 0;
|
|
743
|
-
let messageIndex = 0;
|
|
744
|
-
const items = [];
|
|
745
|
-
const toolDetailMatchCounts = new Map();
|
|
746
|
-
let messageBuffer = [];
|
|
747
|
-
const flushMessageBuffer = () => {
|
|
748
|
-
if (messageBuffer.length === 0)
|
|
749
|
-
return;
|
|
750
|
-
const text = messageBuffer.join('\n').trim();
|
|
751
|
-
if (text) {
|
|
752
|
-
items.push({
|
|
753
|
-
id: `message-${messageIndex}`,
|
|
754
|
-
title: summarizeMessageTitle(text),
|
|
755
|
-
text,
|
|
756
|
-
kind: 'message',
|
|
757
|
-
status: 'completed',
|
|
758
|
-
});
|
|
759
|
-
messageIndex += 1;
|
|
760
|
-
}
|
|
761
|
-
messageBuffer = [];
|
|
762
|
-
};
|
|
763
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
764
|
-
if (finalTextRange && index >= finalTextRange.start && index <= finalTextRange.end)
|
|
765
|
-
continue;
|
|
766
|
-
const line = lines[index];
|
|
767
|
-
if (finalText && isSameMessageText(line, finalText))
|
|
768
|
-
continue;
|
|
769
|
-
if (isSubagentToolLine(line))
|
|
770
|
-
continue;
|
|
771
|
-
if (isToolLikeLine(line)) {
|
|
772
|
-
flushMessageBuffer();
|
|
773
|
-
items.push(buildToolTodo(line, toolIndex, workspaceRoot, toolDetails, toolDetailMatchCounts));
|
|
774
|
-
toolIndex += 1;
|
|
775
|
-
continue;
|
|
776
|
-
}
|
|
777
|
-
if (isFinalAnswerLine(line)) {
|
|
778
|
-
messageBuffer.push(line);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
flushMessageBuffer();
|
|
782
|
-
return items.slice(0, 40);
|
|
783
|
-
}
|
|
784
|
-
function isSameMessageText(left, right) {
|
|
785
|
-
return normalizeMessageText(left) === normalizeMessageText(right);
|
|
786
|
-
}
|
|
787
|
-
function normalizeMessageText(text) {
|
|
788
|
-
return text.trim().replace(/\s+/g, ' ');
|
|
789
|
-
}
|
|
790
|
-
function summarizeMessageTitle(text) {
|
|
791
|
-
const normalized = text.trim().replace(/\s+/g, ' ');
|
|
792
|
-
if (!normalized)
|
|
793
|
-
return 'AI message';
|
|
794
|
-
return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
|
|
795
|
-
}
|
|
796
|
-
function isToolLikeLine(line) {
|
|
797
|
-
return /^(Using|Tool:|Read|Write|Edit|MultiEdit|Bash|Search|Grep|Glob|Todo|Task|Web|Fetch|Claude Code initialized|Codex initialized|.+ running \(\d+s\))/i.test(line.trim());
|
|
798
|
-
}
|
|
799
|
-
function isFinalAnswerLine(line) {
|
|
800
|
-
return !isToolLikeLine(line) && !/^(\[.*\]|Agent runtime configuration:|Conversation history:)/.test(line.trim());
|
|
801
|
-
}
|
|
802
|
-
function isSubagentToolLine(line) {
|
|
803
|
-
return /^Tool:\s*Task\b/i.test(line.trim());
|
|
804
|
-
}
|
|
805
|
-
function buildToolTodo(line, index, workspaceRoot, toolDetails, toolDetailMatchCounts) {
|
|
806
|
-
const summary = summarizeToolLine(line, workspaceRoot);
|
|
807
|
-
const detailId = findToolDetailId(line, toolDetails, toolDetailMatchCounts);
|
|
808
|
-
return {
|
|
809
|
-
id: `tool-${index}`,
|
|
810
|
-
title: summary.title,
|
|
811
|
-
description: summary.description,
|
|
812
|
-
status: 'completed',
|
|
813
|
-
toolName: summary.toolName,
|
|
814
|
-
filePath: summary.filePath,
|
|
815
|
-
command: summary.command,
|
|
816
|
-
detailId,
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
function summarizeToolLine(line, workspaceRoot) {
|
|
820
|
-
const trimmed = line.trim();
|
|
821
|
-
const toolName = extractToolName(trimmed);
|
|
822
|
-
const inputDescription = extractQuotedField(trimmed, 'description');
|
|
823
|
-
const filePath = toWorkspaceRelativePath(extractQuotedField(trimmed, 'file_path') ?? extractQuotedField(trimmed, 'path'), workspaceRoot);
|
|
824
|
-
const command = extractQuotedField(trimmed, 'command') ?? extractCommand(trimmed, toolName);
|
|
825
|
-
const baseName = filePath?.split(/[\\/]/).filter(Boolean).at(-1);
|
|
826
|
-
if (toolName) {
|
|
827
|
-
if (filePath) {
|
|
828
|
-
return {
|
|
829
|
-
title: `${humanizeToolName(toolName)} ${baseName ?? filePath}`,
|
|
830
|
-
description: inputDescription ?? filePath,
|
|
831
|
-
toolName,
|
|
832
|
-
filePath,
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
if (command) {
|
|
836
|
-
return {
|
|
837
|
-
title: `${humanizeToolName(toolName)} command`,
|
|
838
|
-
description: inputDescription,
|
|
839
|
-
toolName,
|
|
840
|
-
command,
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
// Glob/Grep/Search: extract path + pattern + glob for header
|
|
844
|
-
const searchSummary = extractSearchParams(trimmed, toolName, workspaceRoot);
|
|
845
|
-
if (searchSummary) {
|
|
846
|
-
return { ...searchSummary, toolName };
|
|
847
|
-
}
|
|
848
|
-
const todoCount = extractTodoCount(trimmed);
|
|
849
|
-
if (todoCount !== undefined) {
|
|
850
|
-
return {
|
|
851
|
-
title: `Update ${todoCount} ${todoCount === 1 ? 'todo' : 'todos'}`,
|
|
852
|
-
toolName,
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
return {
|
|
856
|
-
title: humanizeToolName(toolName),
|
|
857
|
-
toolName,
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
return { title: trimmed };
|
|
861
|
-
}
|
|
862
|
-
function extractSearchParams(line, toolName, workspaceRoot) {
|
|
863
|
-
if (!/^(Glob|Grep|Search|SemanticSearch|WebSearch|WebFetch|Fetch)$/i.test(toolName))
|
|
864
|
-
return null;
|
|
865
|
-
const pattern = extractQuotedField(line, 'pattern') ?? extractQuotedField(line, 'query');
|
|
866
|
-
const glob = extractQuotedField(line, 'glob');
|
|
867
|
-
const searchPath = toWorkspaceRelativePath(extractQuotedField(line, 'path'), workspaceRoot);
|
|
868
|
-
const url = extractQuotedField(line, 'url');
|
|
869
|
-
const label = humanizeToolName(toolName);
|
|
870
|
-
// WebSearch/WebFetch/Fetch: show url or query
|
|
871
|
-
if (/^(WebSearch|WebFetch|Fetch)$/i.test(toolName)) {
|
|
872
|
-
if (url)
|
|
873
|
-
return { title: `${label} ${truncate(url, 60)}` };
|
|
874
|
-
if (pattern)
|
|
875
|
-
return { title: `${label} ${truncate(pattern, 60)}` };
|
|
876
|
-
return { title: label };
|
|
877
|
-
}
|
|
878
|
-
// Glob: "Find files **/*.ts in src/components"
|
|
879
|
-
if (/^Glob$/i.test(toolName)) {
|
|
880
|
-
const parts = [];
|
|
881
|
-
if (pattern)
|
|
882
|
-
parts.push(truncate(pattern, 50));
|
|
883
|
-
else if (glob)
|
|
884
|
-
parts.push(truncate(glob, 50));
|
|
885
|
-
const title = parts.length ? `${label} ${parts.join(' ')}` : label;
|
|
886
|
-
const descParts = [];
|
|
887
|
-
if (pattern && glob)
|
|
888
|
-
descParts.push(`glob: ${glob}`);
|
|
889
|
-
if (searchPath)
|
|
890
|
-
descParts.push(`in ${searchPath}`);
|
|
891
|
-
return { title, description: descParts.length ? descParts.join(', ') : undefined };
|
|
892
|
-
}
|
|
893
|
-
// Grep/Search/SemanticSearch: "Search pomodoro|timer in src/"
|
|
894
|
-
const parts = [];
|
|
895
|
-
if (pattern)
|
|
896
|
-
parts.push(truncate(pattern, 40));
|
|
897
|
-
const title = parts.length ? `${label} ${parts.join(' ')}` : label;
|
|
898
|
-
const descParts = [];
|
|
899
|
-
if (glob)
|
|
900
|
-
descParts.push(`glob: ${glob}`);
|
|
901
|
-
if (searchPath)
|
|
902
|
-
descParts.push(`in ${searchPath}`);
|
|
903
|
-
return { title, description: descParts.length ? descParts.join(', ') : undefined };
|
|
904
|
-
}
|
|
905
|
-
function truncate(str, max) {
|
|
906
|
-
return str.length > max ? `${str.slice(0, max - 3)}...` : str;
|
|
907
|
-
}
|
|
908
|
-
function extractToolName(line) {
|
|
909
|
-
return line.match(/^Tool:\s*([A-Za-z][\w-]*)\b/)?.[1]
|
|
910
|
-
?? line.match(/^([A-Za-z][\w-]*)\s+running\s+\(\d+s\)/)?.[1]
|
|
911
|
-
?? line.match(/^([A-Za-z][\w-]*):?\s+/)?.[1];
|
|
912
|
-
}
|
|
913
|
-
function humanizeToolName(toolName) {
|
|
914
|
-
const labels = {
|
|
915
|
-
Read: 'Read',
|
|
916
|
-
Write: 'Write',
|
|
917
|
-
Edit: 'Edit',
|
|
918
|
-
MultiEdit: 'Edit',
|
|
919
|
-
Bash: 'Run',
|
|
920
|
-
TodoWrite: 'Update todos',
|
|
921
|
-
Grep: 'Search',
|
|
922
|
-
Glob: 'Find files',
|
|
923
|
-
Search: 'Search',
|
|
924
|
-
SemanticSearch: 'Semantic search',
|
|
925
|
-
WebSearch: 'Web search',
|
|
926
|
-
WebFetch: 'Fetch',
|
|
927
|
-
Fetch: 'Fetch',
|
|
928
|
-
Task: 'Run subagent',
|
|
929
|
-
};
|
|
930
|
-
return labels[toolName] ?? toolName.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
931
|
-
}
|
|
932
|
-
function extractQuotedField(line, key) {
|
|
933
|
-
const quoted = line.match(new RegExp(`\\b${key}=([\"'])(.*?)\\1`))?.[2];
|
|
934
|
-
if (quoted)
|
|
935
|
-
return quoted;
|
|
936
|
-
const json = line.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`))?.[1];
|
|
937
|
-
return json;
|
|
938
|
-
}
|
|
939
|
-
function extractCommand(line, toolName) {
|
|
940
|
-
if (!toolName || !/^(Bash|Shell|Command)$/i.test(toolName))
|
|
941
|
-
return undefined;
|
|
942
|
-
const command = line.replace(/^Tool:\s*/i, '').replace(new RegExp(`^${toolName}:?\\s*`, 'i'), '').trim();
|
|
943
|
-
return command || undefined;
|
|
944
|
-
}
|
|
945
|
-
function extractTodoCount(line) {
|
|
946
|
-
const matches = line.match(/"content"\s*:/g);
|
|
947
|
-
return matches?.length;
|
|
948
|
-
}
|
|
949
|
-
function toWorkspaceRelativePath(path, workspaceRoot) {
|
|
950
|
-
if (!path)
|
|
951
|
-
return undefined;
|
|
952
|
-
if (!workspaceRoot || !path.startsWith(workspaceRoot))
|
|
953
|
-
return path;
|
|
954
|
-
return path.slice(workspaceRoot.length).replace(/^[/\\]/, '');
|
|
955
|
-
}
|
|
956
|
-
function findToolDetailId(line, toolDetails, matchCounts) {
|
|
957
|
-
if (!toolDetails)
|
|
958
|
-
return undefined;
|
|
959
|
-
const targetMatchIndex = matchCounts?.get(line) ?? 0;
|
|
960
|
-
let matchIndex = 0;
|
|
961
|
-
for (const [id, detail] of toolDetails) {
|
|
962
|
-
if (detail.raw !== line)
|
|
963
|
-
continue;
|
|
964
|
-
if (matchIndex === targetMatchIndex) {
|
|
965
|
-
matchCounts?.set(line, targetMatchIndex + 1);
|
|
966
|
-
return id;
|
|
967
|
-
}
|
|
968
|
-
matchIndex += 1;
|
|
969
|
-
}
|
|
970
|
-
return undefined;
|
|
971
|
-
}
|
|
972
|
-
function findToolDetailForResult(toolUseId, result, toolUseDetailIds, toolDetails, workspaceRoot) {
|
|
973
|
-
const detailId = toolUseId ? toolUseDetailIds.get(toolUseId) : undefined;
|
|
974
|
-
const byId = detailId ? toolDetails.get(detailId) : undefined;
|
|
975
|
-
if (byId)
|
|
976
|
-
return byId;
|
|
977
|
-
const resultFilePath = extractResultFilePath(result, workspaceRoot);
|
|
978
|
-
if (resultFilePath) {
|
|
979
|
-
const details = Array.from(toolDetails.values()).reverse();
|
|
980
|
-
for (const detail of details) {
|
|
981
|
-
if (detail.output !== undefined)
|
|
982
|
-
continue;
|
|
983
|
-
const inputFilePath = extractInputFilePath(detail.input, workspaceRoot);
|
|
984
|
-
if (inputFilePath === resultFilePath)
|
|
985
|
-
return detail;
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
if (isSubagentToolResult(result)) {
|
|
989
|
-
const detail = Array.from(toolDetails.values()).reverse().find((candidate) => {
|
|
990
|
-
return candidate.output === undefined && /^Tool:\s*Task\b/i.test(candidate.raw);
|
|
991
|
-
});
|
|
992
|
-
if (detail)
|
|
993
|
-
return detail;
|
|
994
|
-
}
|
|
995
|
-
return Array.from(toolDetails.values()).reverse().find((detail) => detail.output === undefined);
|
|
996
|
-
}
|
|
997
|
-
function isSubagentToolResult(result) {
|
|
998
|
-
if (!result || typeof result !== 'object' || Array.isArray(result))
|
|
999
|
-
return false;
|
|
1000
|
-
const record = result;
|
|
1001
|
-
return (typeof record.agentId === 'string'
|
|
1002
|
-
|| typeof record.agentType === 'string'
|
|
1003
|
-
|| (record.status === 'completed' && Array.isArray(record.content)));
|
|
1004
|
-
}
|
|
1005
|
-
function extractResultFilePath(result, workspaceRoot) {
|
|
1006
|
-
if (!result || typeof result !== 'object')
|
|
1007
|
-
return undefined;
|
|
1008
|
-
const record = result;
|
|
1009
|
-
const file = record.file;
|
|
1010
|
-
if (file && typeof file === 'object') {
|
|
1011
|
-
const filePath = file.filePath;
|
|
1012
|
-
if (typeof filePath === 'string')
|
|
1013
|
-
return toWorkspaceRelativePath(filePath, workspaceRoot);
|
|
1014
|
-
}
|
|
1015
|
-
const filePath = record.filePath;
|
|
1016
|
-
if (typeof filePath === 'string')
|
|
1017
|
-
return toWorkspaceRelativePath(filePath, workspaceRoot);
|
|
1018
|
-
return undefined;
|
|
1019
|
-
}
|
|
1020
|
-
function extractInputFilePath(input, workspaceRoot) {
|
|
1021
|
-
if (!input || typeof input !== 'object')
|
|
1022
|
-
return undefined;
|
|
1023
|
-
const record = input;
|
|
1024
|
-
const filePath = record.file_path ?? record.path;
|
|
1025
|
-
return typeof filePath === 'string' ? toWorkspaceRelativePath(filePath, workspaceRoot) : undefined;
|
|
1026
|
-
}
|
|
1027
|
-
function buildToolDetailId(id, line) {
|
|
1028
|
-
const key = `${id}:${line}`;
|
|
1029
|
-
let hash = 0;
|
|
1030
|
-
for (let index = 0; index < key.length; index += 1) {
|
|
1031
|
-
hash = ((hash << 5) - hash + key.charCodeAt(index)) | 0;
|
|
1032
|
-
}
|
|
1033
|
-
return `tool-${Math.abs(hash).toString(36)}`;
|
|
1034
|
-
}
|
|
1035
|
-
function questionRunKey(workspaceId, channelId, messageId, questionId) {
|
|
1036
|
-
return `${workspaceId}:${channelId}:${messageId}:${questionId}`;
|
|
1037
|
-
}
|
|
1038
|
-
function parseAskUserQuestion(toolUseId, input) {
|
|
1039
|
-
const record = input && typeof input === 'object' ? input : {};
|
|
1040
|
-
const rawQuestions = Array.isArray(record.questions) ? record.questions : [];
|
|
1041
|
-
const first = rawQuestions.find((item) => item && typeof item === 'object');
|
|
1042
|
-
const question = typeof first?.question === 'string' && first.question.trim()
|
|
1043
|
-
? first.question.trim()
|
|
1044
|
-
: '请选择一个选项:';
|
|
1045
|
-
const options = Array.isArray(first?.options) ? first.options : [];
|
|
1046
|
-
const choices = options
|
|
1047
|
-
.map((option) => {
|
|
1048
|
-
if (typeof option === 'string')
|
|
1049
|
-
return option;
|
|
1050
|
-
if (!option || typeof option !== 'object')
|
|
1051
|
-
return '';
|
|
1052
|
-
const value = option;
|
|
1053
|
-
return typeof value.label === 'string' ? value.label : '';
|
|
1054
|
-
})
|
|
1055
|
-
.filter(Boolean);
|
|
1056
|
-
return {
|
|
1057
|
-
id: `ask-user-${toolUseId}`,
|
|
1058
|
-
toolUseId,
|
|
1059
|
-
question,
|
|
1060
|
-
choices,
|
|
1061
|
-
};
|
|
1062
|
-
}
|
|
1063
|
-
function questionFromAnsweredPart(parts, questionId) {
|
|
1064
|
-
const part = parts?.find((item) => item.type === 'ask_user_question' && item.id === questionId);
|
|
1065
|
-
if (!part || part.type !== 'ask_user_question')
|
|
1066
|
-
return undefined;
|
|
1067
|
-
return {
|
|
1068
|
-
id: part.id,
|
|
1069
|
-
toolUseId: part.toolUseId,
|
|
1070
|
-
question: part.question,
|
|
1071
|
-
choices: part.choices ?? [],
|
|
1072
|
-
answer: part.answer,
|
|
1073
|
-
};
|
|
1074
|
-
}
|
|
1075
|
-
function shouldWaitForUserAnswer(questions, summary, error, output) {
|
|
1076
|
-
if (!questions.some((question) => !question.answer))
|
|
1077
|
-
return false;
|
|
1078
|
-
if (summary === 'Waiting for user answer')
|
|
1079
|
-
return true;
|
|
1080
|
-
const text = [error ?? '', ...output].join('\n');
|
|
1081
|
-
return !text.trim() || isAskUserQuestionError(text);
|
|
1082
|
-
}
|
|
1083
|
-
function stripAskUserQuestionErrorLines(output) {
|
|
1084
|
-
return output.filter((line) => !isAskUserQuestionError(line));
|
|
1085
|
-
}
|
|
1086
|
-
function isAskUserQuestionError(error) {
|
|
1087
|
-
return /Answer questions\?/i.test(error);
|
|
1088
|
-
}
|
|
1089
|
-
function extractUsage(lines) {
|
|
1090
|
-
const usage = {};
|
|
1091
|
-
for (const line of lines) {
|
|
1092
|
-
const lower = line.toLowerCase();
|
|
1093
|
-
if (!lower.includes('token'))
|
|
1094
|
-
continue;
|
|
1095
|
-
const input = line.match(/\bin(?:put)?[=:\s]+([\d,]+)/i)?.[1];
|
|
1096
|
-
const output = line.match(/\bout(?:put)?[=:\s]+([\d,]+)/i)?.[1];
|
|
1097
|
-
const total = line.match(/\btokens?[=:\s]+([\d,]+)/i)?.[1];
|
|
1098
|
-
if (input)
|
|
1099
|
-
usage.inputTokens = Number(input.replace(/,/g, ''));
|
|
1100
|
-
if (output)
|
|
1101
|
-
usage.outputTokens = Number(output.replace(/,/g, ''));
|
|
1102
|
-
if (total)
|
|
1103
|
-
usage.totalTokens = Number(total.replace(/,/g, ''));
|
|
1104
|
-
}
|
|
1105
|
-
return usage;
|
|
1106
|
-
}
|
|
1107
|
-
function extractSubagentBlocks(lines, sessionId, toolDetails) {
|
|
1108
|
-
const matchCounts = new Map();
|
|
1109
|
-
return lines
|
|
1110
|
-
.map((line, index) => {
|
|
1111
|
-
const match = line.match(/^Tool:\s*Task\b\s*(.*)$/i);
|
|
1112
|
-
if (!match)
|
|
1113
|
-
return null;
|
|
1114
|
-
const name = line.match(/\bdescription=(["'])(.*?)\1/i)?.[2] || `Subagent ${index + 1}`;
|
|
1115
|
-
const prompt = line.match(/\bprompt=(["'])(.*?)\1/i)?.[2];
|
|
1116
|
-
const detailId = findToolDetailId(line, toolDetails, matchCounts);
|
|
1117
|
-
const detail = detailId ? toolDetails?.get(detailId) : undefined;
|
|
1118
|
-
const output = stringifySubagentOutput(detail?.output);
|
|
1119
|
-
return {
|
|
1120
|
-
id: `subagent-${sessionId}-${index}`,
|
|
1121
|
-
type: 'subagent',
|
|
1122
|
-
name,
|
|
1123
|
-
instructions: prompt,
|
|
1124
|
-
output,
|
|
1125
|
-
};
|
|
1126
|
-
})
|
|
1127
|
-
.filter((part) => Boolean(part));
|
|
1128
|
-
}
|
|
1129
|
-
function stringifySubagentOutput(output) {
|
|
1130
|
-
if (output === undefined || output === null)
|
|
1131
|
-
return undefined;
|
|
1132
|
-
if (typeof output === 'string')
|
|
1133
|
-
return output.trim() || undefined;
|
|
1134
|
-
if (Array.isArray(output)) {
|
|
1135
|
-
const text = output.flatMap((item) => {
|
|
1136
|
-
if (typeof item === 'string')
|
|
1137
|
-
return [item];
|
|
1138
|
-
if (!item || typeof item !== 'object')
|
|
1139
|
-
return [];
|
|
1140
|
-
const record = item;
|
|
1141
|
-
return typeof record.text === 'string' ? [record.text] : [];
|
|
1142
|
-
}).join('\n').trim();
|
|
1143
|
-
return text || undefined;
|
|
1144
|
-
}
|
|
1145
|
-
if (typeof output === 'object') {
|
|
1146
|
-
const record = output;
|
|
1147
|
-
if (typeof record.result === 'string' && record.result.trim())
|
|
1148
|
-
return record.result.trim();
|
|
1149
|
-
if (typeof record.summary === 'string' && record.summary.trim())
|
|
1150
|
-
return record.summary.trim();
|
|
1151
|
-
if (Array.isArray(record.content))
|
|
1152
|
-
return stringifySubagentOutput(record.content);
|
|
1153
|
-
}
|
|
1154
|
-
return undefined;
|
|
1155
|
-
}
|
|
1156
|
-
function buildAgentPrompt(workspaceId, systemPrompt, userPrompt, history = [], runtimeConfig) {
|
|
1157
|
-
const parts = [];
|
|
1158
|
-
const trimmedSystemPrompt = systemPrompt?.trim();
|
|
1159
|
-
if (trimmedSystemPrompt)
|
|
1160
|
-
parts.push(trimmedSystemPrompt);
|
|
1161
|
-
if (runtimeConfig) {
|
|
1162
|
-
const configLines = [
|
|
1163
|
-
'Agent runtime configuration:',
|
|
1164
|
-
`- MCP servers configured for this agent: ${runtimeConfig.mcpServers.length ? runtimeConfig.mcpServers.join(', ') : 'none'}`,
|
|
1165
|
-
`- Skills configured for this agent: ${runtimeConfig.skills.length ? runtimeConfig.skills.join(', ') : 'none'}`,
|
|
1166
|
-
'- Runtime tools available through Claude Code: Read, Write, Edit, MultiEdit, Bash, Grep, Glob, Task, TodoWrite, WebFetch, WebSearch',
|
|
1167
|
-
`- Agent Spaces channel tools configured for this channel: ${runtimeConfig.builtInTools?.length ? runtimeConfig.builtInTools.map((tool) => tool.name).join(', ') : 'none'}`,
|
|
1168
|
-
];
|
|
1169
|
-
if (runtimeConfig.boundDirs?.length) {
|
|
1170
|
-
configLines.push(`- Code directories (boundDirs): ${runtimeConfig.boundDirs.join(', ')}`);
|
|
1171
|
-
}
|
|
1172
|
-
configLines.push('- For Bash commands that create or modify files under the current working directory, use relative paths such as `mkdir -p css js` instead of absolute paths.');
|
|
1173
|
-
if (runtimeConfig.builtInTools?.length) {
|
|
1174
|
-
configLines.push(...formatBuiltInToolContext(runtimeConfig.builtInTools));
|
|
1175
|
-
}
|
|
1176
|
-
if (isIssueContextLookup(userPrompt)) {
|
|
1177
|
-
configLines.push('Current issue lookup rule:', '- The user is asking for the current channel issue.', '- If Agent Spaces channel tools include ViewCurrentChannelIssue, call that function tool first and answer from the tool result.', '- Do not use Bash, Glob, Grep, or project files to infer the current channel issue.', '- If ViewCurrentChannelIssue is not configured but CreateCurrentChannelIssue is configured, say this channel is not bound to an issue yet.', '- If no Agent Spaces channel issue tool is configured, say the issue tool is unavailable.');
|
|
1178
|
-
}
|
|
1179
|
-
configLines.push('When asked what MCP servers, skills, runtime tools, or Agent Spaces channel tools you have, answer from this configuration only. Do not infer availability from provider-side function names or hidden runtime internals.');
|
|
1180
|
-
parts.push(configLines.join('\n'));
|
|
1181
|
-
}
|
|
1182
|
-
if (history.length > 0) {
|
|
1183
|
-
parts.push('Conversation history:');
|
|
1184
|
-
for (const msg of history) {
|
|
1185
|
-
const role = msg.senderId === 'user' ? 'User' : (msg.senderRole || 'Agent');
|
|
1186
|
-
parts.push(`[${role}]: ${stripHtml(msg.content)}`);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
parts.push(`User message:\n${userPrompt}`);
|
|
1190
|
-
return prependWorkspacePrompt(workspaceId, parts.join('\n\n'));
|
|
1191
|
-
}
|
|
1192
|
-
function prependWorkspacePrompt(workspaceId, prompt) {
|
|
1193
|
-
const workspacePrompt = readWorkspacePrompt(workspaceId).trim();
|
|
1194
|
-
if (!workspacePrompt)
|
|
1195
|
-
return prompt;
|
|
1196
|
-
return `${workspacePrompt}\n\n${prompt}`;
|
|
1197
|
-
}
|
|
1198
|
-
function isIssueContextLookup(userPrompt) {
|
|
1199
|
-
const text = stripHtml(userPrompt).toLowerCase();
|
|
1200
|
-
return /当前频道.*议题|议题内容|current channel.*issue|issue.*current channel/.test(text);
|
|
1201
|
-
}
|
|
1202
|
-
function buildBuiltInTools(functionTools, channel, issue) {
|
|
1203
|
-
if (!functionTools.length || !channel)
|
|
1204
|
-
return [];
|
|
1205
|
-
const issueTitle = issue?.title ?? channel.name;
|
|
1206
|
-
return functionTools.map((functionTool) => ({
|
|
1207
|
-
name: functionTool.name,
|
|
1208
|
-
description: functionTool.description,
|
|
1209
|
-
channelId: channel.id,
|
|
1210
|
-
issueId: channel.issueId,
|
|
1211
|
-
issueTitle,
|
|
1212
|
-
}));
|
|
1213
|
-
}
|
|
1214
|
-
function formatBuiltInToolContext(tools) {
|
|
1215
|
-
const firstTool = tools[0];
|
|
1216
|
-
const firstIssueTool = tools.find((tool) => tool.issueId);
|
|
1217
|
-
const lines = [
|
|
1218
|
-
'Built-in issue tool rules:',
|
|
1219
|
-
'- These are real function-call tools exposed through the agent-spaces MCP server.',
|
|
1220
|
-
'- Tool calls must use the current channel id.',
|
|
1221
|
-
];
|
|
1222
|
-
if (firstTool?.channelId)
|
|
1223
|
-
lines.push(`- Current channel id: ${firstTool.channelId}`);
|
|
1224
|
-
if (firstIssueTool?.issueId) {
|
|
1225
|
-
lines.push(`- Current channel issue id: ${firstIssueTool.issueId}`);
|
|
1226
|
-
lines.push(`- Current channel issue title: ${firstIssueTool.issueTitle || 'Untitled issue'}`);
|
|
1227
|
-
}
|
|
1228
|
-
else {
|
|
1229
|
-
lines.push('- Current channel is not bound to an issue yet. Use CreateCurrentChannelIssue before viewing or commenting.');
|
|
1230
|
-
}
|
|
1231
|
-
for (const tool of tools) {
|
|
1232
|
-
lines.push(`- ${tool.name}: ${tool.description}`);
|
|
1233
|
-
}
|
|
1234
|
-
return lines;
|
|
1235
|
-
}
|
|
1236
|
-
function stripHtml(content) {
|
|
1237
|
-
return content
|
|
1238
|
-
.replace(/<span[^>]*data-type=["']mention["'][^>]*data-label=["']([^"']+)["'][^>]*><\/span>/gi, '@$1')
|
|
1239
|
-
.replace(/<[^>]+>/g, ' ')
|
|
1240
|
-
.replace(/ /g, ' ')
|
|
1241
|
-
.replace(/&/g, '&')
|
|
1242
|
-
.replace(/</g, '<')
|
|
1243
|
-
.replace(/>/g, '>')
|
|
1244
|
-
.replace(/\s+/g, ' ')
|
|
1245
|
-
.trim();
|
|
1246
|
-
}
|
|
1247
|
-
function extractMentionIds(content) {
|
|
1248
|
-
const ids = new Set();
|
|
1249
|
-
const mentionPattern = /<span[^>]*data-type=["']mention["'][^>]*>/gi;
|
|
1250
|
-
for (const match of content.matchAll(mentionPattern)) {
|
|
1251
|
-
const id = match[0].match(/\sdata-id=["']([^"']+)["']/i)?.[1];
|
|
1252
|
-
if (id)
|
|
1253
|
-
ids.add(decodeHtml(id));
|
|
1254
|
-
}
|
|
1255
|
-
return [...ids];
|
|
1256
|
-
}
|
|
1257
|
-
function decodeHtml(value) {
|
|
1258
|
-
return value
|
|
1259
|
-
.replace(/"/g, '"')
|
|
1260
|
-
.replace(/'/g, "'")
|
|
1261
|
-
.replace(/&/g, '&')
|
|
1262
|
-
.replace(/</g, '<')
|
|
1263
|
-
.replace(/>/g, '>');
|
|
1264
|
-
}
|
|
75
|
+
registerHandler('channel.answer_question', handleAnswerQuestion);
|
|
1265
76
|
// Register agent handlers
|
|
1266
77
|
registerHandler('agent.start', (_ws, workspaceId, data) => {
|
|
1267
78
|
const { role, issueId } = data;
|
|
1268
|
-
const ctx = makeContext(workspaceId);
|
|
1269
79
|
if (role === 'planner' && issueId) {
|
|
1270
|
-
|
|
1271
|
-
console.error(`[ws] planner error:`, err);
|
|
1272
|
-
});
|
|
80
|
+
console.warn(`[ws] planner start ignored; issue automation is workflow-driven workspaceId=${workspaceId} issueId=${issueId}`);
|
|
1273
81
|
}
|
|
1274
82
|
});
|
|
1275
83
|
registerHandler('agent.stop', (_ws, workspaceId, data) => {
|
|
@@ -1281,5 +89,6 @@ registerHandler('agent.stop', (_ws, workspaceId, data) => {
|
|
|
1281
89
|
to: 'completed',
|
|
1282
90
|
});
|
|
1283
91
|
});
|
|
1284
|
-
export { broadcastToWorkspace };
|
|
92
|
+
export { broadcastToWorkspace } from './connection-manager.js';
|
|
93
|
+
export { stopChannelRuns, hasActiveChannelRuns, markInactiveChannelRunsStopped } from './agent-runner.js';
|
|
1285
94
|
//# sourceMappingURL=handler.js.map
|