@zhanguym/chatroom 1.0.0 → 1.1.0-beta.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/app/application.d.ts +0 -1
- package/dist/app/application.js +8 -7
- package/dist/app/event-bus.d.ts +13 -10
- package/dist/app/external-access-registry.d.ts +1 -0
- package/dist/app/external-access-registry.js +3 -0
- package/dist/app/git-access.d.ts +50 -0
- package/dist/app/git-access.js +2 -0
- package/dist/app/service-registry.d.ts +11 -0
- package/dist/app/service-registry.js +17 -0
- package/dist/app/workspace-access.d.ts +24 -0
- package/dist/app/workspace-access.js +2 -0
- package/dist/auth/ingress-policy.d.ts +2 -0
- package/dist/auth/ingress-policy.js +14 -2
- package/dist/auth/passkey-service.js +1 -1
- package/dist/cli/run-cli.js +1 -1
- package/dist/core/fs/safe-directory.d.ts +9 -0
- package/dist/core/fs/safe-directory.js +129 -0
- package/dist/core/operations/repository.d.ts +2 -0
- package/dist/core/runtime/line-process.d.ts +19 -0
- package/dist/core/runtime/line-process.js +51 -0
- package/dist/infrastructure/database/operation-repository.js +8 -2
- package/dist/infrastructure/database/schema.d.ts +1 -1
- package/dist/infrastructure/database/schema.js +62 -0
- package/dist/infrastructure/http/http-server.js +1 -1
- package/dist/infrastructure/logging/system-log.js +2 -1
- package/dist/native/linux/chatroom-computer-helper +0 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
- package/dist/native/windows/chatroom-computer-helper.exe +0 -0
- package/dist/operations/operation-log.js +16 -4
- package/dist/plugins/agent/async-queue.d.ts +10 -0
- package/dist/plugins/agent/async-queue.js +49 -0
- package/dist/plugins/agent/mcp.d.ts +3 -0
- package/dist/plugins/agent/mcp.js +502 -0
- package/dist/plugins/agent/plugin.d.ts +4 -0
- package/dist/plugins/agent/plugin.js +35 -0
- package/dist/plugins/agent/provider.d.ts +132 -0
- package/dist/plugins/agent/providers/codex/mapping.d.ts +56 -0
- package/dist/plugins/agent/providers/codex/mapping.js +427 -0
- package/dist/plugins/agent/providers/codex/provider.d.ts +84 -0
- package/dist/plugins/agent/providers/codex/provider.js +828 -0
- package/dist/plugins/agent/providers/codex/rpc.d.ts +40 -0
- package/dist/plugins/agent/providers/codex/rpc.js +227 -0
- package/dist/plugins/agent/providers/common.d.ts +11 -0
- package/dist/plugins/agent/providers/common.js +75 -0
- package/dist/plugins/agent/providers/index.d.ts +3 -0
- package/dist/plugins/agent/providers/index.js +4 -0
- package/dist/plugins/agent/repository.d.ts +21 -0
- package/dist/plugins/agent/repository.js +226 -0
- package/dist/plugins/agent/runtime-config.d.ts +5 -0
- package/dist/plugins/agent/runtime-config.js +60 -0
- package/dist/plugins/agent/runtime-files.d.ts +8 -0
- package/dist/plugins/agent/runtime-files.js +170 -0
- package/dist/plugins/agent/runtime-state.d.ts +17 -0
- package/dist/plugins/agent/runtime-state.js +133 -0
- package/dist/plugins/agent/runtime.d.ts +60 -0
- package/dist/plugins/agent/runtime.js +928 -0
- package/dist/plugins/agent/store.d.ts +17 -0
- package/dist/plugins/agent/store.js +1 -0
- package/dist/plugins/agent/types.d.ts +320 -0
- package/dist/plugins/agent/types.js +13 -0
- package/dist/plugins/cloud/plugin.d.ts +1 -1
- package/dist/plugins/cloud/plugin.js +3 -3
- package/dist/plugins/cloud/tunnel-client.js +6 -7
- package/dist/plugins/computer/computer-native-backend.js +9 -1
- package/dist/plugins/computer/computer-native-host.js +21 -5
- package/dist/plugins/computer/computer-schemas.d.ts +7 -7
- package/dist/plugins/computer/computer-service.d.ts +3 -0
- package/dist/plugins/computer/computer-service.js +33 -1
- package/dist/plugins/computer/plugin.d.ts +1 -1
- package/dist/plugins/computer/plugin.js +1 -1
- package/dist/plugins/computer/types.d.ts +1 -0
- package/dist/plugins/git/git-service.d.ts +3 -2
- package/dist/plugins/git/git-service.js +37 -1
- package/dist/plugins/git/plugin.d.ts +0 -2
- package/dist/plugins/git/plugin.js +3 -3
- package/dist/plugins/process/plugin.d.ts +1 -1
- package/dist/plugins/process/plugin.js +3 -3
- package/dist/plugins/process/process-supervisor.d.ts +1 -0
- package/dist/plugins/process/process-supervisor.js +15 -4
- package/dist/plugins/types.d.ts +1 -11
- package/dist/plugins/types.js +1 -17
- package/dist/plugins/web/api-types.d.ts +20 -5
- package/dist/plugins/web/http/agent-api-router.d.ts +3 -0
- package/dist/plugins/web/http/agent-api-router.js +254 -0
- package/dist/plugins/web/http/api-router.js +9 -0
- package/dist/plugins/web/http/computer-api-router.js +21 -6
- package/dist/plugins/web/http/process-api-router.js +3 -0
- package/dist/plugins/web/http/workspace-api-router.js +198 -8
- package/dist/plugins/web/plugin.d.ts +1 -1
- package/dist/plugins/web/plugin.js +5 -4
- package/dist/plugins/web/runtime.d.ts +6 -3
- package/dist/plugins/web/runtime.js +6 -1
- package/dist/plugins/workspace/mcp.d.ts +1 -1
- package/dist/plugins/workspace/mcp.js +125 -1
- package/dist/plugins/workspace/metadata.d.ts +1 -1
- package/dist/plugins/workspace/plugin.d.ts +1 -1
- package/dist/plugins/workspace/plugin.js +9 -3
- package/dist/plugins/workspace/types.d.ts +1 -18
- package/dist/plugins/workspace/workspace-fs.d.ts +19 -3
- package/dist/plugins/workspace/workspace-fs.js +112 -120
- package/dist/plugins/workspace/workspace-root-policy.d.ts +9 -0
- package/dist/plugins/workspace/workspace-root-policy.js +56 -0
- package/dist/plugins/workspace/workspace-service.d.ts +13 -6
- package/dist/plugins/workspace/workspace-service.js +57 -49
- package/dist/web/assets/AgentView-BGLIBoxF.css +1 -0
- package/dist/web/assets/AgentView-BwL3S7sb.js +4 -0
- package/dist/web/assets/CloudView-DH4sOXJr.js +1 -0
- package/dist/web/assets/{CloudView-CHjVbp-5.css → CloudView-DVVvwuWW.css} +1 -1
- package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-BaC1eGzG.js +3 -0
- package/dist/web/assets/ComputerView-BJSjCxFr.js +1 -0
- package/dist/web/assets/ComputerView-DWzfSYJ7.css +1 -0
- package/dist/web/assets/MarkdownContent-AdAmmDn5.js +16 -0
- package/dist/web/assets/MarkdownContent-B_5zTG8c.css +1 -0
- package/dist/web/assets/McpToolsView-CPaWUG9h.js +1 -0
- package/dist/web/assets/McpToolsView-DjxPbTHj.css +1 -0
- package/dist/web/assets/OperationsView-BjTY2lIh.css +1 -0
- package/dist/web/assets/OperationsView-D2FtLkHn.js +4 -0
- package/dist/web/assets/ProcessesView-BHOsaM2H.js +4 -0
- package/dist/web/assets/ProcessesView-Ce6iVwjA.css +1 -0
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-iW1SYlX_.js +1 -0
- package/dist/web/assets/SystemLogsView-By5ijObd.js +1 -0
- package/dist/web/assets/SystemLogsView-CofP4Jvi.css +1 -0
- package/dist/web/assets/VSelect-CLnwhyWJ.js +1 -0
- package/dist/web/assets/VSpacer-dDRRKhUn.js +1 -0
- package/dist/web/assets/VSwitch-CGzxwZuj.js +1 -0
- package/dist/web/assets/VTabs-7aSwjrKs.js +1 -0
- package/dist/web/assets/VTextarea-D1_2FwMe.js +1 -0
- package/dist/web/assets/VTextarea-xiMOHEu-.css +1 -0
- package/dist/web/assets/WorkspaceDownloadPreview-BjuqUdB9.js +1 -0
- package/dist/web/assets/WorkspaceDownloadPreview-DnxhH5Q7.css +1 -0
- package/dist/web/assets/WorkspaceMediaPreview-D-igEnka.css +1 -0
- package/dist/web/assets/WorkspaceMediaPreview-LwXeVOY2.js +3 -0
- package/dist/web/assets/WorkspacesView-ClA6xcjn.css +1 -0
- package/dist/web/assets/WorkspacesView-DHl28eWf.js +2 -0
- package/dist/web/assets/entity-refresh-queue-wQii8Dl5.js +1 -0
- package/dist/web/assets/index-BzRlodie.js +19 -0
- package/dist/web/assets/index-CZ-4tJlF.css +1 -0
- package/dist/web/assets/{syntax-highlight-B5ArnyrC.js → syntax-highlight-C_fuMcwC.js} +1 -1
- package/dist/web/assets/useMasterDetailFocus-BQAutDO9.js +1 -0
- package/dist/web/assets/useOperationLabels-C-zgM_ET.js +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -2
- package/dist/plugins/git/types.d.ts +0 -32
- package/dist/web/assets/CloudView-CvX0dkgu.js +0 -1
- package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-B5TxGkUW.js +0 -3
- package/dist/web/assets/ComputerView-91Z9Cefw.css +0 -1
- package/dist/web/assets/ComputerView-OOyEqWml.js +0 -1
- package/dist/web/assets/McpToolsView-BTnolyS6.css +0 -1
- package/dist/web/assets/McpToolsView-DjXWTKrZ.js +0 -1
- package/dist/web/assets/OperationsView-DqTY51Cr.js +0 -4
- package/dist/web/assets/ProcessesView-C13BykIa.js +0 -4
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DyOhJmR6.js +0 -1
- package/dist/web/assets/SystemLogsView-Ci0u4BTb.js +0 -1
- package/dist/web/assets/VSelect-DrRpKyYI.js +0 -1
- package/dist/web/assets/VSpacer-C3wAY3F0.js +0 -1
- package/dist/web/assets/VSwitch-CxusR4iO.js +0 -1
- package/dist/web/assets/VTabs-qBcuuXh3.js +0 -1
- package/dist/web/assets/WorkspacesView-BzQ0aH74.css +0 -1
- package/dist/web/assets/WorkspacesView-CijIvfsp.js +0 -2
- package/dist/web/assets/index-BegKQ3Yd.js +0 -19
- package/dist/web/assets/index-CkjqXRSj.css +0 -1
- package/dist/web/assets/useOperationLabels-BDjzGDkT.js +0 -1
- /package/dist/{plugins/process → core/runtime}/head-tail-buffer.d.ts +0 -0
- /package/dist/{plugins/process → core/runtime}/head-tail-buffer.js +0 -0
- /package/dist/plugins/{git/types.js → agent/provider.js} +0 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ChatRoomError } from "#core/errors/chatroom-error";
|
|
3
|
+
import { HeadTailBuffer } from "#core/runtime/head-tail-buffer";
|
|
4
|
+
import { approvalPolicySupported, copyAgentSessionConfig, emptyAgentSessionConfig, validateReviewTarget, } from "./runtime-config.js";
|
|
5
|
+
import { cleanupOrphanAttachments as cleanupOrphanAttachmentFiles, cleanupSessionFiles, prepareProviderItem, validateAttachments, } from "./runtime-files.js";
|
|
6
|
+
import { applyItemDelta, buildContinuityDelta, materializeInteraction, materializeProviderItem, mergeContinuityContext, nextSessionUpdatedAt, structuredError, } from "./runtime-state.js";
|
|
7
|
+
import { isAgentProviderId, isAgentPermissionMode } from "./types.js";
|
|
8
|
+
const AGENT_COMMAND_OUTPUT_LIMIT_BYTES = 512 * 1024;
|
|
9
|
+
export class AgentRuntime {
|
|
10
|
+
store;
|
|
11
|
+
workspaces;
|
|
12
|
+
events;
|
|
13
|
+
providerSessions = new Map();
|
|
14
|
+
activeTurns = new Map();
|
|
15
|
+
sessionMutations = new Set();
|
|
16
|
+
deltaInvalidations = new Map();
|
|
17
|
+
closed = false;
|
|
18
|
+
providers;
|
|
19
|
+
constructor(store, workspaces, events, providers) {
|
|
20
|
+
this.store = store;
|
|
21
|
+
this.workspaces = workspaces;
|
|
22
|
+
this.events = events;
|
|
23
|
+
this.providers = new Map();
|
|
24
|
+
for (const provider of providers) {
|
|
25
|
+
if (!isAgentProviderId(provider.id))
|
|
26
|
+
throw new Error("Invalid Agent provider id: " + provider.id);
|
|
27
|
+
if (this.providers.has(provider.id))
|
|
28
|
+
throw new Error("Duplicate Agent provider id: " + provider.id);
|
|
29
|
+
this.providers.set(provider.id, provider);
|
|
30
|
+
}
|
|
31
|
+
this.store.reconcileInterrupted(new Date().toISOString());
|
|
32
|
+
}
|
|
33
|
+
async providerStatuses() {
|
|
34
|
+
return Promise.all([...this.providers.values()].map(async (provider) => ({
|
|
35
|
+
id: provider.id,
|
|
36
|
+
displayName: provider.displayName,
|
|
37
|
+
icon: provider.icon,
|
|
38
|
+
features: provider.features,
|
|
39
|
+
nativeSettings: provider.nativeSettings,
|
|
40
|
+
...(await provider.probe()),
|
|
41
|
+
})));
|
|
42
|
+
}
|
|
43
|
+
async providerDetails(providerId) {
|
|
44
|
+
return this.requireProvider(providerId).details();
|
|
45
|
+
}
|
|
46
|
+
async models(providerId) {
|
|
47
|
+
return this.requireProvider(providerId).listModels();
|
|
48
|
+
}
|
|
49
|
+
listSessions() {
|
|
50
|
+
return this.store.listSessions();
|
|
51
|
+
}
|
|
52
|
+
getSession(sessionId) {
|
|
53
|
+
return this.requireSession(sessionId);
|
|
54
|
+
}
|
|
55
|
+
history(sessionId) {
|
|
56
|
+
this.persistPendingSession(sessionId);
|
|
57
|
+
const history = this.store.history(sessionId);
|
|
58
|
+
if (!history)
|
|
59
|
+
throw new ChatRoomError("NOT_FOUND", "Unknown Agent session: " + sessionId);
|
|
60
|
+
return history;
|
|
61
|
+
}
|
|
62
|
+
historyUpdate(sessionId, turnIds, itemIds) {
|
|
63
|
+
this.persistPendingSession(sessionId);
|
|
64
|
+
const update = this.store.historyUpdate(sessionId, turnIds, itemIds);
|
|
65
|
+
if (!update)
|
|
66
|
+
throw new ChatRoomError("NOT_FOUND", "Unknown Agent session: " + sessionId);
|
|
67
|
+
return update;
|
|
68
|
+
}
|
|
69
|
+
async createSession(input) {
|
|
70
|
+
const workspace = await this.workspaces.info(input.workspaceRoot);
|
|
71
|
+
const workspaceRoot = workspace.root;
|
|
72
|
+
const provider = this.requireProvider(input.provider);
|
|
73
|
+
const providerStatus = await provider.probe();
|
|
74
|
+
if (!providerStatus.installed)
|
|
75
|
+
throw new ChatRoomError("UNSUPPORTED", input.provider + " is not installed");
|
|
76
|
+
if (!providerStatus.authenticated)
|
|
77
|
+
throw new ChatRoomError("FORBIDDEN", input.provider + " is not authenticated", providerStatus.error ? { error: providerStatus.error } : undefined);
|
|
78
|
+
const config = await this.validateConfig(provider, emptyAgentSessionConfig(), input);
|
|
79
|
+
const context = {
|
|
80
|
+
workspaceRoot,
|
|
81
|
+
workspacePrompt: workspace.presetPrompt,
|
|
82
|
+
...config,
|
|
83
|
+
continuityContext: null,
|
|
84
|
+
continuityDelta: null,
|
|
85
|
+
};
|
|
86
|
+
const providerSession = await provider.createSession(context);
|
|
87
|
+
const now = new Date().toISOString();
|
|
88
|
+
const session = {
|
|
89
|
+
id: "agent_" + randomUUID(),
|
|
90
|
+
workspaceRoot,
|
|
91
|
+
provider: input.provider,
|
|
92
|
+
...config,
|
|
93
|
+
tokenUsage: null,
|
|
94
|
+
status: "idle",
|
|
95
|
+
attention: null,
|
|
96
|
+
createdAt: now,
|
|
97
|
+
updatedAt: now,
|
|
98
|
+
};
|
|
99
|
+
this.store.insertSession(session);
|
|
100
|
+
this.store.upsertProviderState({
|
|
101
|
+
sessionId: session.id,
|
|
102
|
+
provider: session.provider,
|
|
103
|
+
providerSessionId: providerSession.providerSessionId,
|
|
104
|
+
...copyAgentSessionConfig(session),
|
|
105
|
+
tokenUsage: null,
|
|
106
|
+
continuityContext: "",
|
|
107
|
+
syncedThroughTurnId: null,
|
|
108
|
+
createdAt: now,
|
|
109
|
+
updatedAt: now,
|
|
110
|
+
});
|
|
111
|
+
this.providerSessions.set(session.id, providerSession);
|
|
112
|
+
this.invalidateSession(session.id);
|
|
113
|
+
return session;
|
|
114
|
+
}
|
|
115
|
+
async startTurn(sessionId, input, source) {
|
|
116
|
+
return this.withSessionMutation(sessionId, () => this.startTurnUnlocked(sessionId, input, source));
|
|
117
|
+
}
|
|
118
|
+
async startTurnUnlocked(sessionId, input, source) {
|
|
119
|
+
const session = this.requireSession(sessionId);
|
|
120
|
+
if (this.activeTurns.has(sessionId) || session.status === "running")
|
|
121
|
+
throw new ChatRoomError("CONFLICT", "Agent session already has an active turn");
|
|
122
|
+
const text = input.text.trim();
|
|
123
|
+
if (!text && input.attachments.length === 0)
|
|
124
|
+
throw new ChatRoomError("INVALID_INPUT", "Agent message is empty");
|
|
125
|
+
const attachments = await validateAttachments(session, input.attachments);
|
|
126
|
+
await this.validateInputModalities(session, text, attachments);
|
|
127
|
+
const providerSession = await this.providerSession(session);
|
|
128
|
+
const active = this.beginTurn(session, providerSession, source, session.serviceTier, { text, attachments });
|
|
129
|
+
void this.executeTurn(session, active, () => providerSession.runTurn({
|
|
130
|
+
text,
|
|
131
|
+
attachments,
|
|
132
|
+
outputSchema: input.outputSchema ?? null,
|
|
133
|
+
}));
|
|
134
|
+
return active.turn;
|
|
135
|
+
}
|
|
136
|
+
async startReview(sessionId, target, source) {
|
|
137
|
+
return this.withSessionMutation(sessionId, async () => {
|
|
138
|
+
const session = this.requireSession(sessionId);
|
|
139
|
+
if (this.activeTurns.has(sessionId) || session.status === "running")
|
|
140
|
+
throw new ChatRoomError("CONFLICT", "Agent session already has an active turn");
|
|
141
|
+
const provider = this.requireProvider(session.provider);
|
|
142
|
+
if (!provider.features.nativeReview)
|
|
143
|
+
throw new ChatRoomError("UNSUPPORTED", session.provider + " does not support native review turns");
|
|
144
|
+
validateReviewTarget(target);
|
|
145
|
+
const providerSession = await this.providerSession(session);
|
|
146
|
+
if (!providerSession.runReview)
|
|
147
|
+
throw new ChatRoomError("INTERNAL", session.provider + " declares native review without implementing it");
|
|
148
|
+
const runReview = providerSession.runReview.bind(providerSession);
|
|
149
|
+
const active = this.beginTurn(session, providerSession, source, session.serviceTier, null);
|
|
150
|
+
void this.executeTurn(session, active, () => runReview(target));
|
|
151
|
+
return active.turn;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async steer(sessionId, input, source) {
|
|
155
|
+
return this.withSessionMutation(sessionId, async () => {
|
|
156
|
+
const active = this.activeTurns.get(sessionId);
|
|
157
|
+
if (!active)
|
|
158
|
+
throw new ChatRoomError("CONFLICT", "Agent session has no active turn");
|
|
159
|
+
const session = this.requireSession(sessionId);
|
|
160
|
+
const text = input.text.trim();
|
|
161
|
+
if (!text && input.attachments.length === 0)
|
|
162
|
+
throw new ChatRoomError("INVALID_INPUT", "Agent message is empty");
|
|
163
|
+
const attachments = await validateAttachments(session, input.attachments);
|
|
164
|
+
await this.validateInputModalities(session, text, attachments);
|
|
165
|
+
if (this.activeTurns.get(sessionId) !== active)
|
|
166
|
+
throw new ChatRoomError("CONFLICT", "Agent turn ended before steering");
|
|
167
|
+
const provider = this.requireProvider(session.provider);
|
|
168
|
+
if (!provider.features.steering)
|
|
169
|
+
throw new ChatRoomError("UNSUPPORTED", session.provider + " does not support steering an active turn");
|
|
170
|
+
if (!active.providerSession.steer)
|
|
171
|
+
throw new ChatRoomError("INTERNAL", session.provider + " declares steering without implementing it");
|
|
172
|
+
const sequence = active.nextSequence++;
|
|
173
|
+
await active.providerSession.steer({ text, attachments });
|
|
174
|
+
const now = new Date().toISOString();
|
|
175
|
+
const item = {
|
|
176
|
+
id: "item_" + randomUUID(),
|
|
177
|
+
turnId: active.turn.id,
|
|
178
|
+
providerItemId: null,
|
|
179
|
+
sequence,
|
|
180
|
+
status: "completed",
|
|
181
|
+
type: "user_message",
|
|
182
|
+
source,
|
|
183
|
+
text,
|
|
184
|
+
attachments,
|
|
185
|
+
createdAt: now,
|
|
186
|
+
updatedAt: now,
|
|
187
|
+
};
|
|
188
|
+
this.store.insertItem(item);
|
|
189
|
+
this.invalidateSession(sessionId, {
|
|
190
|
+
turnIds: [active.turn.id],
|
|
191
|
+
itemIds: [item.id],
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
beginTurn(session, providerSession, source, serviceTier, userInput) {
|
|
196
|
+
const now = new Date().toISOString();
|
|
197
|
+
const turn = {
|
|
198
|
+
id: "turn_" + randomUUID(),
|
|
199
|
+
sessionId: session.id,
|
|
200
|
+
providerTurnId: null,
|
|
201
|
+
provider: session.provider,
|
|
202
|
+
...copyAgentSessionConfig(session),
|
|
203
|
+
serviceTier,
|
|
204
|
+
usage: null,
|
|
205
|
+
source,
|
|
206
|
+
status: "running",
|
|
207
|
+
error: null,
|
|
208
|
+
startedAt: now,
|
|
209
|
+
completedAt: null,
|
|
210
|
+
};
|
|
211
|
+
this.store.insertTurn(turn);
|
|
212
|
+
const userItem = userInput
|
|
213
|
+
? {
|
|
214
|
+
id: "item_" + randomUUID(),
|
|
215
|
+
turnId: turn.id,
|
|
216
|
+
providerItemId: null,
|
|
217
|
+
sequence: 0,
|
|
218
|
+
status: "completed",
|
|
219
|
+
type: "user_message",
|
|
220
|
+
source,
|
|
221
|
+
...userInput,
|
|
222
|
+
createdAt: now,
|
|
223
|
+
updatedAt: now,
|
|
224
|
+
}
|
|
225
|
+
: null;
|
|
226
|
+
if (userItem)
|
|
227
|
+
this.store.insertItem(userItem);
|
|
228
|
+
const active = {
|
|
229
|
+
turn,
|
|
230
|
+
providerSession,
|
|
231
|
+
items: new Map(),
|
|
232
|
+
commandOutputs: new Map(),
|
|
233
|
+
interactions: new Map(),
|
|
234
|
+
respondingInteractions: new Set(),
|
|
235
|
+
nextSequence: userInput ? 1 : 0,
|
|
236
|
+
};
|
|
237
|
+
this.activeTurns.set(session.id, active);
|
|
238
|
+
this.updateSessionStatus(session, "running", {
|
|
239
|
+
turnIds: [turn.id],
|
|
240
|
+
itemIds: userItem ? [userItem.id] : [],
|
|
241
|
+
});
|
|
242
|
+
return active;
|
|
243
|
+
}
|
|
244
|
+
async configureSession(sessionId, patch) {
|
|
245
|
+
return this.withSessionMutation(sessionId, () => this.configureSessionUnlocked(sessionId, patch));
|
|
246
|
+
}
|
|
247
|
+
async configureSessionUnlocked(sessionId, patch) {
|
|
248
|
+
const session = this.requireSession(sessionId);
|
|
249
|
+
if (this.activeTurns.has(sessionId) || session.status !== "idle")
|
|
250
|
+
throw new ChatRoomError("CONFLICT", "Cannot configure an active Agent session");
|
|
251
|
+
const config = await this.validateConfig(this.requireProvider(session.provider), copyAgentSessionConfig(session), patch);
|
|
252
|
+
const providerSession = await this.providerSession(session);
|
|
253
|
+
await providerSession.configure(config);
|
|
254
|
+
const updated = {
|
|
255
|
+
...session,
|
|
256
|
+
...config,
|
|
257
|
+
updatedAt: nextSessionUpdatedAt(session.updatedAt),
|
|
258
|
+
};
|
|
259
|
+
const providerState = this.requireProviderState(sessionId, session.provider);
|
|
260
|
+
this.store.upsertProviderState({
|
|
261
|
+
...providerState,
|
|
262
|
+
...config,
|
|
263
|
+
providerSessionId: providerSession.providerSessionId,
|
|
264
|
+
updatedAt: updated.updatedAt,
|
|
265
|
+
});
|
|
266
|
+
this.store.updateSession(updated);
|
|
267
|
+
this.invalidateSession(sessionId);
|
|
268
|
+
return updated;
|
|
269
|
+
}
|
|
270
|
+
async validateConfig(provider, current, patch) {
|
|
271
|
+
const config = { ...current };
|
|
272
|
+
for (const key of [
|
|
273
|
+
"model",
|
|
274
|
+
"reasoningEffort",
|
|
275
|
+
"reasoningSummary",
|
|
276
|
+
"serviceTier",
|
|
277
|
+
"approvalPolicy",
|
|
278
|
+
"approvalsReviewer",
|
|
279
|
+
"permissionMode",
|
|
280
|
+
]) {
|
|
281
|
+
if (patch[key] !== undefined)
|
|
282
|
+
Object.assign(config, { [key]: patch[key] });
|
|
283
|
+
}
|
|
284
|
+
const approvalPolicySetting = provider.nativeSettings.find((setting) => setting.id === "approvalPolicy");
|
|
285
|
+
const approvalsReviewerSetting = provider.nativeSettings.find((setting) => setting.id === "approvalsReviewer");
|
|
286
|
+
const permissionModeSetting = provider.nativeSettings.find((setting) => setting.id === "permissionMode");
|
|
287
|
+
if (approvalPolicySetting) {
|
|
288
|
+
config.approvalPolicy ??=
|
|
289
|
+
approvalPolicySetting.defaultValue === "untrusted" ||
|
|
290
|
+
approvalPolicySetting.defaultValue === "on-request" ||
|
|
291
|
+
approvalPolicySetting.defaultValue === "never"
|
|
292
|
+
? approvalPolicySetting.defaultValue
|
|
293
|
+
: "on-request";
|
|
294
|
+
if (!approvalPolicySupported(config.approvalPolicy, approvalPolicySetting))
|
|
295
|
+
throw new ChatRoomError("INVALID_INPUT", "Unsupported approvalPolicy for " + provider.id);
|
|
296
|
+
}
|
|
297
|
+
else if (config.approvalPolicy !== null) {
|
|
298
|
+
throw new ChatRoomError("INVALID_INPUT", provider.id + " does not support approvalPolicy");
|
|
299
|
+
}
|
|
300
|
+
if (approvalsReviewerSetting) {
|
|
301
|
+
config.approvalsReviewer ??=
|
|
302
|
+
approvalsReviewerSetting.defaultValue === "auto_review" ||
|
|
303
|
+
approvalsReviewerSetting.defaultValue === "guardian_subagent"
|
|
304
|
+
? approvalsReviewerSetting.defaultValue
|
|
305
|
+
: "user";
|
|
306
|
+
if (!approvalsReviewerSetting.options.some((option) => option.id === config.approvalsReviewer))
|
|
307
|
+
throw new ChatRoomError("INVALID_INPUT", "Unsupported approvalsReviewer for " + provider.id);
|
|
308
|
+
}
|
|
309
|
+
else if (config.approvalsReviewer !== null) {
|
|
310
|
+
throw new ChatRoomError("INVALID_INPUT", provider.id + " does not support approvalsReviewer");
|
|
311
|
+
}
|
|
312
|
+
if (permissionModeSetting) {
|
|
313
|
+
config.permissionMode ??= isAgentPermissionMode(permissionModeSetting.defaultValue)
|
|
314
|
+
? permissionModeSetting.defaultValue
|
|
315
|
+
: "workspace-write";
|
|
316
|
+
if (!permissionModeSetting.options.some((option) => option.id === config.permissionMode))
|
|
317
|
+
throw new ChatRoomError("INVALID_INPUT", "Unsupported permissionMode for " + provider.id);
|
|
318
|
+
}
|
|
319
|
+
else if (config.permissionMode !== null) {
|
|
320
|
+
throw new ChatRoomError("INVALID_INPUT", provider.id + " does not support permissionMode");
|
|
321
|
+
}
|
|
322
|
+
const models = await provider.listModels();
|
|
323
|
+
const model = config.model === null
|
|
324
|
+
? (models.find((candidate) => candidate.isDefault) ?? models[0])
|
|
325
|
+
: models.find((candidate) => candidate.id === config.model);
|
|
326
|
+
if (config.model !== null && !model)
|
|
327
|
+
throw new ChatRoomError("INVALID_INPUT", "Unknown model for " + provider.id + ": " + config.model);
|
|
328
|
+
const supportsEffort = (value) => value === null ||
|
|
329
|
+
!!model?.reasoningEfforts.some((effort) => effort.id === value);
|
|
330
|
+
const supportsTier = (value) => value === null || !!model?.serviceTiers.some((tier) => tier.id === value);
|
|
331
|
+
if (config.model !== current.model) {
|
|
332
|
+
if (patch.reasoningEffort === undefined &&
|
|
333
|
+
!supportsEffort(config.reasoningEffort))
|
|
334
|
+
config.reasoningEffort = supportsEffort(model?.defaultReasoningEffort ?? null)
|
|
335
|
+
? (model?.defaultReasoningEffort ?? null)
|
|
336
|
+
: null;
|
|
337
|
+
if (patch.serviceTier === undefined && !supportsTier(config.serviceTier))
|
|
338
|
+
config.serviceTier = supportsTier(model?.defaultServiceTier ?? null)
|
|
339
|
+
? (model?.defaultServiceTier ?? null)
|
|
340
|
+
: null;
|
|
341
|
+
if (patch.reasoningSummary === undefined &&
|
|
342
|
+
!model?.reasoningSummaries.includes(config.reasoningSummary ?? "none"))
|
|
343
|
+
config.reasoningSummary =
|
|
344
|
+
model?.defaultReasoningSummary &&
|
|
345
|
+
model.reasoningSummaries.includes(model.defaultReasoningSummary)
|
|
346
|
+
? model.defaultReasoningSummary
|
|
347
|
+
: null;
|
|
348
|
+
}
|
|
349
|
+
if (!supportsEffort(config.reasoningEffort))
|
|
350
|
+
throw new ChatRoomError("INVALID_INPUT", "Unsupported reasoning effort: " + config.reasoningEffort);
|
|
351
|
+
if (!supportsTier(config.serviceTier))
|
|
352
|
+
throw new ChatRoomError("INVALID_INPUT", "Unsupported service tier: " + config.serviceTier);
|
|
353
|
+
if (config.reasoningSummary !== null &&
|
|
354
|
+
!model?.reasoningSummaries.includes(config.reasoningSummary))
|
|
355
|
+
throw new ChatRoomError("INVALID_INPUT", "Unsupported reasoning summary: " + config.reasoningSummary);
|
|
356
|
+
return config;
|
|
357
|
+
}
|
|
358
|
+
async interrupt(sessionId) {
|
|
359
|
+
const active = this.activeTurns.get(sessionId);
|
|
360
|
+
if (!active)
|
|
361
|
+
throw new ChatRoomError("CONFLICT", "Agent session has no active turn");
|
|
362
|
+
await active.providerSession.interrupt();
|
|
363
|
+
}
|
|
364
|
+
async respondInteraction(sessionId, itemId, response) {
|
|
365
|
+
const active = this.activeTurns.get(sessionId);
|
|
366
|
+
if (!active)
|
|
367
|
+
throw new ChatRoomError("CONFLICT", "Agent session has no active turn");
|
|
368
|
+
const providerInteractionId = active.interactions.get(itemId);
|
|
369
|
+
if (!providerInteractionId)
|
|
370
|
+
throw new ChatRoomError("NOT_FOUND", "Unknown agent interaction: " + itemId);
|
|
371
|
+
const item = [...active.items.values()].find((value) => value.id === itemId);
|
|
372
|
+
if (!item || item.type !== "interaction")
|
|
373
|
+
throw new ChatRoomError("NOT_FOUND", "Unknown agent interaction: " + itemId);
|
|
374
|
+
if (response.type !== item.interaction.kind)
|
|
375
|
+
throw new ChatRoomError("INVALID_INPUT", "Interaction response type does not match the request");
|
|
376
|
+
if (response.type === "approval" &&
|
|
377
|
+
!item.interaction.approvalOptions.some((option) => option.id === response.decision))
|
|
378
|
+
throw new ChatRoomError("INVALID_INPUT", "Unknown approval option: " + response.decision);
|
|
379
|
+
if (active.respondingInteractions.has(itemId))
|
|
380
|
+
throw new ChatRoomError("CONFLICT", "Agent interaction response is already being submitted");
|
|
381
|
+
active.respondingInteractions.add(itemId);
|
|
382
|
+
try {
|
|
383
|
+
await active.providerSession.resolveInteraction(providerInteractionId, response);
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
active.respondingInteractions.delete(itemId);
|
|
387
|
+
}
|
|
388
|
+
active.interactions.delete(itemId);
|
|
389
|
+
const current = active.items.get(item.providerItemId ?? item.id);
|
|
390
|
+
const updated = {
|
|
391
|
+
...item,
|
|
392
|
+
status: current?.status === "interrupted" || current?.status === "failed"
|
|
393
|
+
? current.status
|
|
394
|
+
: "completed",
|
|
395
|
+
interaction: { ...item.interaction, response },
|
|
396
|
+
updatedAt: new Date().toISOString(),
|
|
397
|
+
};
|
|
398
|
+
active.items.set(item.providerItemId ?? item.id, updated);
|
|
399
|
+
this.persistItem(sessionId, updated);
|
|
400
|
+
if (this.activeTurns.get(sessionId) === active)
|
|
401
|
+
this.updateSessionStatus(this.requireSession(sessionId), active.interactions.size ? "waiting_input" : "running", {
|
|
402
|
+
turnIds: [active.turn.id],
|
|
403
|
+
itemIds: [updated.id],
|
|
404
|
+
});
|
|
405
|
+
else
|
|
406
|
+
this.invalidateSession(sessionId, {
|
|
407
|
+
turnIds: [active.turn.id],
|
|
408
|
+
itemIds: [updated.id],
|
|
409
|
+
});
|
|
410
|
+
return updated;
|
|
411
|
+
}
|
|
412
|
+
async deleteSession(sessionId) {
|
|
413
|
+
return this.withSessionMutation(sessionId, () => this.deleteSessionUnlocked(sessionId));
|
|
414
|
+
}
|
|
415
|
+
async deleteSessionUnlocked(sessionId) {
|
|
416
|
+
if (this.activeTurns.has(sessionId))
|
|
417
|
+
throw new ChatRoomError("CONFLICT", "Cannot delete a running Agent session");
|
|
418
|
+
const history = this.history(sessionId);
|
|
419
|
+
const providerSession = this.providerSessions.get(sessionId);
|
|
420
|
+
if (providerSession) {
|
|
421
|
+
await providerSession.close().catch(() => undefined);
|
|
422
|
+
this.providerSessions.delete(sessionId);
|
|
423
|
+
}
|
|
424
|
+
await cleanupSessionFiles(this.store, history);
|
|
425
|
+
this.store.deleteSession(sessionId);
|
|
426
|
+
this.invalidateSession(sessionId, { deleted: true });
|
|
427
|
+
}
|
|
428
|
+
async switchProvider(sessionId, targetProvider, patch = {}) {
|
|
429
|
+
return this.withSessionMutation(sessionId, () => this.switchProviderUnlocked(sessionId, targetProvider, patch));
|
|
430
|
+
}
|
|
431
|
+
async switchProviderUnlocked(sessionId, targetProvider, patch = {}) {
|
|
432
|
+
const session = this.requireSession(sessionId);
|
|
433
|
+
if (this.activeTurns.has(sessionId) || session.status !== "idle")
|
|
434
|
+
throw new ChatRoomError("CONFLICT", "Wait for the current turn to finish before switching agents");
|
|
435
|
+
if (session.provider === targetProvider)
|
|
436
|
+
return this.configureSessionUnlocked(sessionId, patch);
|
|
437
|
+
const workspace = await this.workspaces.info(session.workspaceRoot);
|
|
438
|
+
const workspaceRoot = workspace.root;
|
|
439
|
+
const provider = this.requireProvider(targetProvider);
|
|
440
|
+
const providerStatus = await provider.probe();
|
|
441
|
+
if (!providerStatus.installed)
|
|
442
|
+
throw new ChatRoomError("UNSUPPORTED", targetProvider + " is not installed");
|
|
443
|
+
if (!providerStatus.authenticated)
|
|
444
|
+
throw new ChatRoomError("FORBIDDEN", targetProvider + " is not authenticated", providerStatus.error ? { error: providerStatus.error } : undefined);
|
|
445
|
+
const existingState = this.store.getProviderState(sessionId, targetProvider);
|
|
446
|
+
const config = await this.validateConfig(provider, existingState
|
|
447
|
+
? copyAgentSessionConfig(existingState)
|
|
448
|
+
: emptyAgentSessionConfig(), patch);
|
|
449
|
+
const history = this.history(sessionId);
|
|
450
|
+
const latestTurnId = history.turns.at(-1)?.turn.id ?? null;
|
|
451
|
+
const delta = buildContinuityDelta(history, existingState?.syncedThroughTurnId ?? null);
|
|
452
|
+
const continuityContext = mergeContinuityContext(existingState?.continuityContext ?? "", delta);
|
|
453
|
+
const currentState = this.requireProviderState(sessionId, session.provider);
|
|
454
|
+
this.store.upsertProviderState({
|
|
455
|
+
...currentState,
|
|
456
|
+
...copyAgentSessionConfig(session),
|
|
457
|
+
tokenUsage: session.tokenUsage,
|
|
458
|
+
syncedThroughTurnId: latestTurnId,
|
|
459
|
+
createdAt: currentState.createdAt,
|
|
460
|
+
updatedAt: new Date().toISOString(),
|
|
461
|
+
});
|
|
462
|
+
const context = {
|
|
463
|
+
workspaceRoot,
|
|
464
|
+
workspacePrompt: workspace.presetPrompt,
|
|
465
|
+
...config,
|
|
466
|
+
continuityContext: continuityContext || null,
|
|
467
|
+
continuityDelta: delta || null,
|
|
468
|
+
};
|
|
469
|
+
const currentProviderSession = this.providerSessions.get(sessionId);
|
|
470
|
+
if (currentProviderSession) {
|
|
471
|
+
await currentProviderSession.close().catch(() => undefined);
|
|
472
|
+
this.providerSessions.delete(sessionId);
|
|
473
|
+
}
|
|
474
|
+
const providerSession = existingState
|
|
475
|
+
? await provider.resumeSession(context, existingState.providerSessionId)
|
|
476
|
+
: await provider.createSession(context);
|
|
477
|
+
const now = new Date().toISOString();
|
|
478
|
+
const providerState = {
|
|
479
|
+
sessionId,
|
|
480
|
+
provider: targetProvider,
|
|
481
|
+
providerSessionId: providerSession.providerSessionId,
|
|
482
|
+
...config,
|
|
483
|
+
tokenUsage: existingState?.tokenUsage ?? null,
|
|
484
|
+
continuityContext,
|
|
485
|
+
syncedThroughTurnId: latestTurnId,
|
|
486
|
+
createdAt: existingState?.createdAt ?? now,
|
|
487
|
+
updatedAt: now,
|
|
488
|
+
};
|
|
489
|
+
this.store.upsertProviderState(providerState);
|
|
490
|
+
this.providerSessions.set(sessionId, providerSession);
|
|
491
|
+
const updated = {
|
|
492
|
+
...session,
|
|
493
|
+
provider: targetProvider,
|
|
494
|
+
...config,
|
|
495
|
+
tokenUsage: existingState?.tokenUsage ?? null,
|
|
496
|
+
status: "idle",
|
|
497
|
+
attention: null,
|
|
498
|
+
updatedAt: nextSessionUpdatedAt(session.updatedAt),
|
|
499
|
+
};
|
|
500
|
+
this.store.updateSession(updated);
|
|
501
|
+
this.invalidateSession(updated.id);
|
|
502
|
+
return updated;
|
|
503
|
+
}
|
|
504
|
+
async close() {
|
|
505
|
+
this.closed = true;
|
|
506
|
+
for (const pending of this.deltaInvalidations.values()) {
|
|
507
|
+
clearTimeout(pending.timer);
|
|
508
|
+
this.persistPendingItems(pending);
|
|
509
|
+
}
|
|
510
|
+
this.deltaInvalidations.clear();
|
|
511
|
+
for (const active of this.activeTurns.values())
|
|
512
|
+
await active.providerSession.interrupt().catch(() => undefined);
|
|
513
|
+
this.activeTurns.clear();
|
|
514
|
+
for (const session of this.providerSessions.values())
|
|
515
|
+
await session.close().catch(() => undefined);
|
|
516
|
+
this.providerSessions.clear();
|
|
517
|
+
await Promise.all([...this.providers.values()].map((provider) => provider.close().catch(() => undefined)));
|
|
518
|
+
}
|
|
519
|
+
async executeTurn(session, active, events) {
|
|
520
|
+
try {
|
|
521
|
+
for await (const event of events()) {
|
|
522
|
+
if (this.activeTurns.get(session.id) !== active)
|
|
523
|
+
break;
|
|
524
|
+
await this.handleProviderEvent(session, active, event);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
if (this.activeTurns.get(session.id) === active)
|
|
529
|
+
this.finishTurn(session, active, "failed", structuredError(error));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async handleProviderEvent(session, active, event) {
|
|
533
|
+
if (event.type === "turn.started") {
|
|
534
|
+
active.turn = {
|
|
535
|
+
...active.turn,
|
|
536
|
+
providerTurnId: event.providerTurnId,
|
|
537
|
+
};
|
|
538
|
+
this.store.updateTurn(active.turn);
|
|
539
|
+
this.invalidateSession(session.id, { turnIds: [active.turn.id] });
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (event.type === "item.started") {
|
|
543
|
+
const existing = active.items.get(event.providerItemId);
|
|
544
|
+
if (existing)
|
|
545
|
+
return;
|
|
546
|
+
const materialized = materializeProviderItem(active.turn.id, event.providerItemId, active.nextSequence++, "running", event.item);
|
|
547
|
+
const item = this.boundCommandOutput(active, event.providerItemId, materialized);
|
|
548
|
+
active.items.set(event.providerItemId, item);
|
|
549
|
+
this.store.insertItem(item);
|
|
550
|
+
this.invalidateSession(session.id, {
|
|
551
|
+
turnIds: [active.turn.id],
|
|
552
|
+
itemIds: [item.id],
|
|
553
|
+
});
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (event.type === "item.delta") {
|
|
557
|
+
const item = active.items.get(event.providerItemId);
|
|
558
|
+
if (!item)
|
|
559
|
+
return;
|
|
560
|
+
const updated = event.field === "output" && item.type === "command"
|
|
561
|
+
? this.appendCommandOutput(active, event.providerItemId, item, event.delta)
|
|
562
|
+
: applyItemDelta(item, event.field, event.delta);
|
|
563
|
+
active.items.set(event.providerItemId, updated);
|
|
564
|
+
this.scheduleDeltaInvalidation(session.id, {
|
|
565
|
+
turnIds: [active.turn.id],
|
|
566
|
+
itemIds: [updated.id],
|
|
567
|
+
}, updated);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (event.type === "item.updated") {
|
|
571
|
+
const existing = active.items.get(event.providerItemId);
|
|
572
|
+
const payload = await prepareProviderItem(session, event.item);
|
|
573
|
+
const updated = materializeProviderItem(active.turn.id, event.providerItemId, existing?.sequence ?? active.nextSequence++, existing?.status ?? "running", payload, existing);
|
|
574
|
+
active.items.set(event.providerItemId, updated);
|
|
575
|
+
const bounded = this.boundCommandOutput(active, event.providerItemId, updated);
|
|
576
|
+
active.items.set(event.providerItemId, bounded);
|
|
577
|
+
if (existing)
|
|
578
|
+
this.persistItem(session.id, bounded);
|
|
579
|
+
else
|
|
580
|
+
this.store.insertItem(bounded);
|
|
581
|
+
this.invalidateSession(session.id, {
|
|
582
|
+
turnIds: [active.turn.id],
|
|
583
|
+
itemIds: [bounded.id],
|
|
584
|
+
});
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (event.type === "token.usage") {
|
|
588
|
+
const state = this.requireProviderState(session.id, session.provider);
|
|
589
|
+
const now = new Date().toISOString();
|
|
590
|
+
this.store.upsertProviderState({
|
|
591
|
+
...state,
|
|
592
|
+
tokenUsage: event.usage,
|
|
593
|
+
updatedAt: now,
|
|
594
|
+
});
|
|
595
|
+
const currentSession = this.requireSession(session.id);
|
|
596
|
+
Object.assign(session, currentSession, {
|
|
597
|
+
tokenUsage: event.usage,
|
|
598
|
+
updatedAt: nextSessionUpdatedAt(currentSession.updatedAt),
|
|
599
|
+
});
|
|
600
|
+
this.store.updateSession(session);
|
|
601
|
+
active.turn = { ...active.turn, usage: event.usage.last };
|
|
602
|
+
this.store.updateTurn(active.turn);
|
|
603
|
+
this.scheduleDeltaInvalidation(session.id, {
|
|
604
|
+
turnIds: [active.turn.id],
|
|
605
|
+
});
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (event.type === "model.rerouted") {
|
|
609
|
+
active.turn = { ...active.turn, model: event.toModel };
|
|
610
|
+
this.store.updateTurn(active.turn);
|
|
611
|
+
const id = "reroute_" + randomUUID();
|
|
612
|
+
const notice = materializeProviderItem(active.turn.id, id, active.nextSequence++, "completed", {
|
|
613
|
+
type: "notice",
|
|
614
|
+
level: "info",
|
|
615
|
+
text: `Model rerouted from ${event.fromModel} to ${event.toModel}: ${event.reason}`,
|
|
616
|
+
});
|
|
617
|
+
active.items.set(id, notice);
|
|
618
|
+
this.store.insertItem(notice);
|
|
619
|
+
this.invalidateSession(session.id, {
|
|
620
|
+
turnIds: [active.turn.id],
|
|
621
|
+
itemIds: [notice.id],
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (event.type === "item.completed") {
|
|
626
|
+
const existing = active.items.get(event.providerItemId);
|
|
627
|
+
const providerItem = await prepareProviderItem(session, event.item);
|
|
628
|
+
const completed = materializeProviderItem(active.turn.id, event.providerItemId, existing?.sequence ?? active.nextSequence++, event.failed ? "failed" : "completed", providerItem, existing);
|
|
629
|
+
active.items.set(event.providerItemId, completed);
|
|
630
|
+
const bounded = this.boundCommandOutput(active, event.providerItemId, completed);
|
|
631
|
+
active.items.set(event.providerItemId, bounded);
|
|
632
|
+
if (existing)
|
|
633
|
+
this.persistItem(session.id, bounded);
|
|
634
|
+
else
|
|
635
|
+
this.store.insertItem(bounded);
|
|
636
|
+
this.invalidateSession(session.id, {
|
|
637
|
+
turnIds: [active.turn.id],
|
|
638
|
+
itemIds: [bounded.id],
|
|
639
|
+
});
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (event.type === "interaction.resolved") {
|
|
643
|
+
const entry = [...active.interactions].find(([, requestId]) => requestId === event.providerRequestId);
|
|
644
|
+
if (!entry)
|
|
645
|
+
return;
|
|
646
|
+
const [itemId] = entry;
|
|
647
|
+
active.interactions.delete(itemId);
|
|
648
|
+
const item = [...active.items.values()].find((candidate) => candidate.id === itemId);
|
|
649
|
+
if (item?.type === "interaction") {
|
|
650
|
+
const updated = {
|
|
651
|
+
...item,
|
|
652
|
+
status: "completed",
|
|
653
|
+
updatedAt: new Date().toISOString(),
|
|
654
|
+
};
|
|
655
|
+
active.items.set(item.providerItemId ?? item.id, updated);
|
|
656
|
+
this.persistItem(session.id, updated);
|
|
657
|
+
}
|
|
658
|
+
this.updateSessionStatus(session, active.interactions.size ? "waiting_input" : "running", {
|
|
659
|
+
turnIds: [active.turn.id],
|
|
660
|
+
itemIds: item ? [item.id] : [],
|
|
661
|
+
});
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (event.type === "interaction.requested") {
|
|
665
|
+
const item = materializeInteraction(active.turn.id, event.providerItemId, active.nextSequence++, event.interaction);
|
|
666
|
+
active.items.set(event.providerItemId, item);
|
|
667
|
+
active.interactions.set(item.id, event.interaction.id);
|
|
668
|
+
this.store.insertItem(item);
|
|
669
|
+
this.updateSessionStatus(session, "waiting_input", {
|
|
670
|
+
turnIds: [active.turn.id],
|
|
671
|
+
itemIds: [item.id],
|
|
672
|
+
});
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (event.type === "turn.completed")
|
|
676
|
+
this.finishTurn(session, active, event.status, event.error ?? null);
|
|
677
|
+
}
|
|
678
|
+
finishTurn(session, active, status, error) {
|
|
679
|
+
if (this.activeTurns.get(session.id) !== active)
|
|
680
|
+
return;
|
|
681
|
+
const now = new Date().toISOString();
|
|
682
|
+
const changedItemIds = [];
|
|
683
|
+
for (const [key, item] of active.items) {
|
|
684
|
+
if (item.status !== "running" && item.status !== "waiting")
|
|
685
|
+
continue;
|
|
686
|
+
const updated = {
|
|
687
|
+
...item,
|
|
688
|
+
status: status === "completed" ? "interrupted" : status,
|
|
689
|
+
updatedAt: now,
|
|
690
|
+
};
|
|
691
|
+
active.items.set(key, updated);
|
|
692
|
+
this.persistItem(session.id, updated);
|
|
693
|
+
changedItemIds.push(updated.id);
|
|
694
|
+
}
|
|
695
|
+
active.turn = {
|
|
696
|
+
...active.turn,
|
|
697
|
+
status,
|
|
698
|
+
error,
|
|
699
|
+
completedAt: now,
|
|
700
|
+
};
|
|
701
|
+
this.store.updateTurn(active.turn);
|
|
702
|
+
const providerState = this.requireProviderState(session.id, session.provider);
|
|
703
|
+
this.store.upsertProviderState({
|
|
704
|
+
...providerState,
|
|
705
|
+
syncedThroughTurnId: active.turn.id,
|
|
706
|
+
createdAt: providerState.createdAt,
|
|
707
|
+
updatedAt: now,
|
|
708
|
+
});
|
|
709
|
+
this.activeTurns.delete(session.id);
|
|
710
|
+
this.updateSessionStatus(session, "idle", {
|
|
711
|
+
turnIds: [active.turn.id],
|
|
712
|
+
itemIds: changedItemIds,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
async providerSession(session) {
|
|
716
|
+
const workspace = await this.workspaces.info(session.workspaceRoot);
|
|
717
|
+
const workspaceRoot = workspace.root;
|
|
718
|
+
const existing = this.providerSessions.get(session.id);
|
|
719
|
+
if (existing)
|
|
720
|
+
return existing;
|
|
721
|
+
const provider = this.requireProvider(session.provider);
|
|
722
|
+
const providerState = this.requireProviderState(session.id, session.provider);
|
|
723
|
+
try {
|
|
724
|
+
const resumed = await provider.resumeSession({
|
|
725
|
+
workspaceRoot,
|
|
726
|
+
workspacePrompt: workspace.presetPrompt,
|
|
727
|
+
...copyAgentSessionConfig(providerState),
|
|
728
|
+
continuityContext: providerState.continuityContext || null,
|
|
729
|
+
continuityDelta: null,
|
|
730
|
+
}, providerState.providerSessionId);
|
|
731
|
+
this.providerSessions.set(session.id, resumed);
|
|
732
|
+
return resumed;
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
this.updateSessionStatus(session, "error");
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
requireProviderState(sessionId, provider) {
|
|
740
|
+
const state = this.store.getProviderState(sessionId, provider);
|
|
741
|
+
if (!state)
|
|
742
|
+
throw new ChatRoomError("INTERNAL", `Missing provider state for agent session ${sessionId} (${provider})`);
|
|
743
|
+
return state;
|
|
744
|
+
}
|
|
745
|
+
updateSessionStatus(session, status, change = {}) {
|
|
746
|
+
const current = this.requireSession(session.id);
|
|
747
|
+
const updated = {
|
|
748
|
+
...current,
|
|
749
|
+
status,
|
|
750
|
+
updatedAt: nextSessionUpdatedAt(current.updatedAt),
|
|
751
|
+
};
|
|
752
|
+
Object.assign(session, updated);
|
|
753
|
+
this.store.updateSession(updated);
|
|
754
|
+
this.invalidateSession(updated.id, change);
|
|
755
|
+
return updated;
|
|
756
|
+
}
|
|
757
|
+
requireProvider(providerId) {
|
|
758
|
+
const provider = this.providers.get(providerId);
|
|
759
|
+
if (!provider)
|
|
760
|
+
throw new ChatRoomError("UNSUPPORTED", "Unsupported Agent provider: " + providerId);
|
|
761
|
+
return provider;
|
|
762
|
+
}
|
|
763
|
+
requireSession(sessionId) {
|
|
764
|
+
const session = this.store.getSession(sessionId);
|
|
765
|
+
if (!session)
|
|
766
|
+
throw new ChatRoomError("NOT_FOUND", "Unknown Agent session: " + sessionId);
|
|
767
|
+
return session;
|
|
768
|
+
}
|
|
769
|
+
async cleanupOrphanAttachments(maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
|
|
770
|
+
await cleanupOrphanAttachmentFiles(this.store, maxAgeMs);
|
|
771
|
+
}
|
|
772
|
+
async withSessionMutation(sessionId, action) {
|
|
773
|
+
if (this.sessionMutations.has(sessionId))
|
|
774
|
+
throw new ChatRoomError("CONFLICT", "Agent session is already being modified");
|
|
775
|
+
this.sessionMutations.add(sessionId);
|
|
776
|
+
try {
|
|
777
|
+
return await action();
|
|
778
|
+
}
|
|
779
|
+
finally {
|
|
780
|
+
this.sessionMutations.delete(sessionId);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
async validateInputModalities(session, text, attachments) {
|
|
784
|
+
const models = await this.requireProvider(session.provider).listModels();
|
|
785
|
+
const model = session.model === null
|
|
786
|
+
? (models.find((candidate) => candidate.isDefault) ?? models[0])
|
|
787
|
+
: models.find((candidate) => candidate.id === session.model);
|
|
788
|
+
if (!model)
|
|
789
|
+
return;
|
|
790
|
+
if ((text ||
|
|
791
|
+
attachments.some((attachment) => attachment.kind === "file" || attachment.kind === "skill")) &&
|
|
792
|
+
!model.inputModalities.includes("text"))
|
|
793
|
+
throw new ChatRoomError("INVALID_INPUT", "Selected model does not support text input");
|
|
794
|
+
if (attachments.some((attachment) => attachment.kind === "image") &&
|
|
795
|
+
!model.inputModalities.includes("image"))
|
|
796
|
+
throw new ChatRoomError("INVALID_INPUT", "Selected model does not support image input");
|
|
797
|
+
if (attachments.some((attachment) => attachment.kind === "audio") &&
|
|
798
|
+
!model.inputModalities.includes("audio"))
|
|
799
|
+
throw new ChatRoomError("INVALID_INPUT", "Selected model does not support audio input");
|
|
800
|
+
}
|
|
801
|
+
scheduleDeltaInvalidation(sessionId, change = {}, persistedItem) {
|
|
802
|
+
if (this.closed)
|
|
803
|
+
return;
|
|
804
|
+
const existing = this.deltaInvalidations.get(sessionId);
|
|
805
|
+
if (existing) {
|
|
806
|
+
mergeInvalidation(existing, change);
|
|
807
|
+
if (persistedItem)
|
|
808
|
+
existing.persistedItems.set(persistedItem.id, persistedItem);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const pending = {
|
|
812
|
+
timer: setTimeout(() => {
|
|
813
|
+
try {
|
|
814
|
+
this.flushDeltaInvalidation(sessionId);
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
this.failActiveTurn(sessionId, error);
|
|
818
|
+
}
|
|
819
|
+
}, 150),
|
|
820
|
+
turnIds: new Set(change.turnIds ?? []),
|
|
821
|
+
itemIds: new Set(change.itemIds ?? []),
|
|
822
|
+
persistedItems: new Map(persistedItem ? [[persistedItem.id, persistedItem]] : []),
|
|
823
|
+
deleted: Boolean(change.deleted),
|
|
824
|
+
};
|
|
825
|
+
pending.timer.unref();
|
|
826
|
+
this.deltaInvalidations.set(sessionId, pending);
|
|
827
|
+
}
|
|
828
|
+
flushDeltaInvalidation(sessionId) {
|
|
829
|
+
const pending = this.deltaInvalidations.get(sessionId);
|
|
830
|
+
if (!pending)
|
|
831
|
+
return;
|
|
832
|
+
this.deltaInvalidations.delete(sessionId);
|
|
833
|
+
this.persistPendingItems(pending);
|
|
834
|
+
if (this.closed)
|
|
835
|
+
return;
|
|
836
|
+
this.events.emit({
|
|
837
|
+
type: "agent",
|
|
838
|
+
sessionId,
|
|
839
|
+
...(pending.turnIds.size ? { turnIds: [...pending.turnIds] } : {}),
|
|
840
|
+
...(pending.itemIds.size ? { itemIds: [...pending.itemIds] } : {}),
|
|
841
|
+
...(pending.deleted ? { deleted: true } : {}),
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
persistItem(sessionId, item) {
|
|
845
|
+
this.deltaInvalidations.get(sessionId)?.persistedItems.delete(item.id);
|
|
846
|
+
this.store.updateItem(item);
|
|
847
|
+
}
|
|
848
|
+
persistPendingSession(sessionId) {
|
|
849
|
+
const pending = this.deltaInvalidations.get(sessionId);
|
|
850
|
+
if (pending)
|
|
851
|
+
this.persistPendingItems(pending);
|
|
852
|
+
}
|
|
853
|
+
failActiveTurn(sessionId, error) {
|
|
854
|
+
const active = this.activeTurns.get(sessionId);
|
|
855
|
+
if (!active)
|
|
856
|
+
return;
|
|
857
|
+
const session = this.store.getSession(sessionId);
|
|
858
|
+
if (!session)
|
|
859
|
+
return;
|
|
860
|
+
try {
|
|
861
|
+
this.finishTurn(session, active, "failed", structuredError(error));
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
this.activeTurns.delete(sessionId);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
persistPendingItems(pending) {
|
|
868
|
+
for (const item of pending.persistedItems.values())
|
|
869
|
+
this.store.updateItem(item);
|
|
870
|
+
pending.persistedItems.clear();
|
|
871
|
+
}
|
|
872
|
+
boundCommandOutput(active, providerItemId, item) {
|
|
873
|
+
if (item.type !== "command") {
|
|
874
|
+
active.commandOutputs.delete(providerItemId);
|
|
875
|
+
return item;
|
|
876
|
+
}
|
|
877
|
+
const buffer = new HeadTailBuffer(AGENT_COMMAND_OUTPUT_LIMIT_BYTES);
|
|
878
|
+
buffer.append(item.output);
|
|
879
|
+
active.commandOutputs.set(providerItemId, buffer);
|
|
880
|
+
return { ...item, output: buffer.snapshot().text };
|
|
881
|
+
}
|
|
882
|
+
appendCommandOutput(active, providerItemId, item, delta) {
|
|
883
|
+
let buffer = active.commandOutputs.get(providerItemId);
|
|
884
|
+
if (!buffer) {
|
|
885
|
+
buffer = new HeadTailBuffer(AGENT_COMMAND_OUTPUT_LIMIT_BYTES);
|
|
886
|
+
buffer.append(item.output);
|
|
887
|
+
active.commandOutputs.set(providerItemId, buffer);
|
|
888
|
+
}
|
|
889
|
+
buffer.append(delta);
|
|
890
|
+
return {
|
|
891
|
+
...item,
|
|
892
|
+
output: buffer.snapshot().text,
|
|
893
|
+
updatedAt: new Date().toISOString(),
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
invalidateSession(sessionId, change = {}) {
|
|
897
|
+
const pending = this.deltaInvalidations.get(sessionId);
|
|
898
|
+
const turnIds = new Set(change.turnIds ?? []);
|
|
899
|
+
const itemIds = new Set(change.itemIds ?? []);
|
|
900
|
+
let deleted = Boolean(change.deleted);
|
|
901
|
+
if (pending) {
|
|
902
|
+
clearTimeout(pending.timer);
|
|
903
|
+
this.deltaInvalidations.delete(sessionId);
|
|
904
|
+
this.persistPendingItems(pending);
|
|
905
|
+
for (const id of pending.turnIds)
|
|
906
|
+
turnIds.add(id);
|
|
907
|
+
for (const id of pending.itemIds)
|
|
908
|
+
itemIds.add(id);
|
|
909
|
+
deleted ||= pending.deleted;
|
|
910
|
+
}
|
|
911
|
+
if (this.closed)
|
|
912
|
+
return;
|
|
913
|
+
this.events.emit({
|
|
914
|
+
type: "agent",
|
|
915
|
+
sessionId,
|
|
916
|
+
...(turnIds.size ? { turnIds: [...turnIds] } : {}),
|
|
917
|
+
...(itemIds.size ? { itemIds: [...itemIds] } : {}),
|
|
918
|
+
...(deleted ? { deleted: true } : {}),
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function mergeInvalidation(target, change) {
|
|
923
|
+
for (const id of change.turnIds ?? [])
|
|
924
|
+
target.turnIds.add(id);
|
|
925
|
+
for (const id of change.itemIds ?? [])
|
|
926
|
+
target.itemIds.add(id);
|
|
927
|
+
target.deleted ||= Boolean(change.deleted);
|
|
928
|
+
}
|