@clinebot/core 0.0.33 → 0.0.34
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/auth/client.d.ts +19 -0
- package/dist/auth/client.d.ts.map +1 -1
- package/dist/auth/cline.d.ts.map +1 -1
- package/dist/auth/oca.d.ts.map +1 -1
- package/dist/auth/server.d.ts +32 -0
- package/dist/auth/server.d.ts.map +1 -1
- package/dist/auth/types.d.ts +29 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/extensions/index.d.ts +2 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-config-loader.d.ts +2 -1
- package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-load-report.d.ts +19 -0
- package/dist/extensions/plugin/plugin-load-report.d.ts.map +1 -0
- package/dist/extensions/plugin/plugin-loader.d.ts +6 -0
- package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-sandbox.d.ts +2 -1
- package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
- package/dist/extensions/plugin-sandbox-bootstrap.js +148 -148
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +207 -207
- package/dist/runtime/runtime-builder.d.ts +1 -1
- package/dist/runtime/runtime-builder.d.ts.map +1 -1
- package/dist/runtime/subprocess-sandbox.d.ts +2 -0
- package/dist/runtime/subprocess-sandbox.d.ts.map +1 -1
- package/dist/runtime/tool-approval.d.ts.map +1 -1
- package/dist/session/default-session-manager.d.ts.map +1 -1
- package/dist/session/persistence-service.d.ts.map +1 -1
- package/dist/session/session-artifacts.d.ts +2 -0
- package/dist/session/session-artifacts.d.ts.map +1 -1
- package/dist/session/session-config-builder.d.ts.map +1 -1
- package/dist/team/team-tools.d.ts.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/auth/client.test.ts +29 -0
- package/src/auth/client.ts +21 -0
- package/src/auth/cline.ts +2 -0
- package/src/auth/oca.ts +2 -0
- package/src/auth/server.test.ts +287 -0
- package/src/auth/server.ts +50 -1
- package/src/auth/types.ts +29 -0
- package/src/extensions/index.ts +6 -0
- package/src/extensions/plugin/plugin-config-loader.test.ts +37 -0
- package/src/extensions/plugin/plugin-config-loader.ts +18 -10
- package/src/extensions/plugin/plugin-load-report.ts +20 -0
- package/src/extensions/plugin/plugin-loader.test.ts +45 -0
- package/src/extensions/plugin/plugin-loader.ts +57 -3
- package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +158 -86
- package/src/extensions/plugin/plugin-sandbox.test.ts +70 -0
- package/src/extensions/plugin/plugin-sandbox.ts +17 -6
- package/src/index.ts +11 -0
- package/src/runtime/hook-file-hooks.test.ts +42 -7
- package/src/runtime/runtime-builder.test.ts +98 -0
- package/src/runtime/runtime-builder.ts +112 -65
- package/src/runtime/subprocess-sandbox.ts +26 -23
- package/src/runtime/tool-approval.ts +13 -15
- package/src/session/default-session-manager.ts +1 -3
- package/src/session/persistence-service.test.ts +38 -0
- package/src/session/persistence-service.ts +16 -1
- package/src/session/session-artifacts.ts +16 -0
- package/src/session/session-config-builder.ts +46 -0
- package/src/team/team-tools.test.ts +104 -0
- package/src/team/team-tools.ts +35 -16
- package/src/types/config.ts +1 -0
- package/dist/runtime/team-runtime-registry.d.ts +0 -13
- package/dist/runtime/team-runtime-registry.d.ts.map +0 -1
- package/src/runtime/team-runtime-registry.ts +0 -43
|
@@ -61,6 +61,22 @@ export class SubprocessSandbox {
|
|
|
61
61
|
this.options = options;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
private get processLabel(): string {
|
|
65
|
+
return this.options.name ?? "sandbox";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private clearPendingRequest(id: string): PendingRequest | undefined {
|
|
69
|
+
const pending = this.pending.get(id);
|
|
70
|
+
if (!pending) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
this.pending.delete(id);
|
|
74
|
+
if (pending.timeout) {
|
|
75
|
+
clearTimeout(pending.timeout);
|
|
76
|
+
}
|
|
77
|
+
return pending;
|
|
78
|
+
}
|
|
79
|
+
|
|
64
80
|
start(): void {
|
|
65
81
|
if (this.process && this.process.exitCode === null) {
|
|
66
82
|
return;
|
|
@@ -96,7 +112,7 @@ export class SubprocessSandbox {
|
|
|
96
112
|
child.on("error", (error) => {
|
|
97
113
|
this.failPending(
|
|
98
114
|
new Error(
|
|
99
|
-
`${this.
|
|
115
|
+
`${this.processLabel} process error: ${asError(error).message}`,
|
|
100
116
|
),
|
|
101
117
|
);
|
|
102
118
|
});
|
|
@@ -119,9 +135,7 @@ export class SubprocessSandbox {
|
|
|
119
135
|
this.start();
|
|
120
136
|
const child = this.process;
|
|
121
137
|
if (!child || child.exitCode !== null) {
|
|
122
|
-
throw new Error(
|
|
123
|
-
`${this.options.name ?? "sandbox"} process is not available`,
|
|
124
|
-
);
|
|
138
|
+
throw new Error(`${this.processLabel} process is not available`);
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
const id = `req_${++this.requestCounter}`;
|
|
@@ -139,13 +153,13 @@ export class SubprocessSandbox {
|
|
|
139
153
|
};
|
|
140
154
|
if ((options.timeoutMs ?? 0) > 0) {
|
|
141
155
|
pending.timeout = setTimeout(() => {
|
|
142
|
-
this.
|
|
156
|
+
this.clearPendingRequest(id);
|
|
143
157
|
this.shutdown().catch(() => {
|
|
144
158
|
// Best-effort process shutdown after timeout.
|
|
145
159
|
});
|
|
146
160
|
reject(
|
|
147
161
|
new Error(
|
|
148
|
-
`${this.
|
|
162
|
+
`${this.processLabel} call timed out after ${options.timeoutMs}ms: ${method}`,
|
|
149
163
|
),
|
|
150
164
|
);
|
|
151
165
|
}, options.timeoutMs);
|
|
@@ -155,17 +169,13 @@ export class SubprocessSandbox {
|
|
|
155
169
|
if (!error) {
|
|
156
170
|
return;
|
|
157
171
|
}
|
|
158
|
-
const entry = this.
|
|
172
|
+
const entry = this.clearPendingRequest(id);
|
|
159
173
|
if (!entry) {
|
|
160
174
|
return;
|
|
161
175
|
}
|
|
162
|
-
this.pending.delete(id);
|
|
163
|
-
if (entry.timeout) {
|
|
164
|
-
clearTimeout(entry.timeout);
|
|
165
|
-
}
|
|
166
176
|
entry.reject(
|
|
167
177
|
new Error(
|
|
168
|
-
`${this.
|
|
178
|
+
`${this.processLabel} failed to send call "${method}": ${asError(error).message}`,
|
|
169
179
|
),
|
|
170
180
|
);
|
|
171
181
|
});
|
|
@@ -176,7 +186,7 @@ export class SubprocessSandbox {
|
|
|
176
186
|
const child = this.process;
|
|
177
187
|
this.process = null;
|
|
178
188
|
if (!child || child.exitCode !== null) {
|
|
179
|
-
this.failPending(new Error(`${this.
|
|
189
|
+
this.failPending(new Error(`${this.processLabel} shutdown`));
|
|
180
190
|
return;
|
|
181
191
|
}
|
|
182
192
|
await new Promise<void>((resolve) => {
|
|
@@ -199,7 +209,7 @@ export class SubprocessSandbox {
|
|
|
199
209
|
resolve();
|
|
200
210
|
}
|
|
201
211
|
});
|
|
202
|
-
this.failPending(new Error(`${this.
|
|
212
|
+
this.failPending(new Error(`${this.processLabel} shutdown`));
|
|
203
213
|
}
|
|
204
214
|
|
|
205
215
|
private onMessage(
|
|
@@ -220,23 +230,16 @@ export class SubprocessSandbox {
|
|
|
220
230
|
if (message.type !== "response" || !message.id) {
|
|
221
231
|
return;
|
|
222
232
|
}
|
|
223
|
-
const pending = this.
|
|
233
|
+
const pending = this.clearPendingRequest(message.id);
|
|
224
234
|
if (!pending) {
|
|
225
235
|
return;
|
|
226
236
|
}
|
|
227
|
-
this.pending.delete(message.id);
|
|
228
|
-
if (pending.timeout) {
|
|
229
|
-
clearTimeout(pending.timeout);
|
|
230
|
-
}
|
|
231
237
|
if (message.ok) {
|
|
232
238
|
pending.resolve(message.result);
|
|
233
239
|
return;
|
|
234
240
|
}
|
|
235
241
|
pending.reject(
|
|
236
|
-
new Error(
|
|
237
|
-
message.error?.message ||
|
|
238
|
-
`${this.options.name ?? "sandbox"} call failed`,
|
|
239
|
-
),
|
|
242
|
+
new Error(message.error?.message || `${this.processLabel} call failed`),
|
|
240
243
|
);
|
|
241
244
|
}
|
|
242
245
|
|
|
@@ -18,6 +18,14 @@ function delay(ms: number): Promise<void> {
|
|
|
18
18
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
async function unlinkIfPresent(path: string): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
await unlink(path);
|
|
24
|
+
} catch {
|
|
25
|
+
// Best-effort cleanup.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export async function requestDesktopToolApproval(
|
|
22
30
|
request: ToolApprovalRequest,
|
|
23
31
|
options: DesktopToolApprovalOptions = {},
|
|
@@ -77,16 +85,10 @@ export async function requestDesktopToolApproval(
|
|
|
77
85
|
approved: parsed.approved === true,
|
|
78
86
|
reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
|
|
79
87
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
try {
|
|
86
|
-
await unlink(requestPath);
|
|
87
|
-
} catch {
|
|
88
|
-
// Best-effort cleanup.
|
|
89
|
-
}
|
|
88
|
+
await Promise.all([
|
|
89
|
+
unlinkIfPresent(decisionPath),
|
|
90
|
+
unlinkIfPresent(requestPath),
|
|
91
|
+
]);
|
|
90
92
|
return result;
|
|
91
93
|
} catch {
|
|
92
94
|
// Decision not available yet.
|
|
@@ -94,11 +96,7 @@ export async function requestDesktopToolApproval(
|
|
|
94
96
|
await delay(pollIntervalMs);
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
await unlink(requestPath);
|
|
99
|
-
} catch {
|
|
100
|
-
// Best-effort cleanup.
|
|
101
|
-
}
|
|
99
|
+
await unlinkIfPresent(requestPath);
|
|
102
100
|
|
|
103
101
|
return { approved: false, reason: "Tool approval request timed out" };
|
|
104
102
|
}
|
|
@@ -1391,9 +1391,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
1391
1391
|
});
|
|
1392
1392
|
} catch (error) {
|
|
1393
1393
|
if (error instanceof OAuthReauthRequiredError) {
|
|
1394
|
-
throw new Error(
|
|
1395
|
-
`OAuth session for "${error.providerId}" requires re-authentication. Run "clite auth ${error.providerId}" and retry.`,
|
|
1396
|
-
);
|
|
1394
|
+
throw new Error(`${error.providerId} requires re-authentication.`);
|
|
1397
1395
|
}
|
|
1398
1396
|
throw error;
|
|
1399
1397
|
}
|
|
@@ -200,4 +200,42 @@ describe("UnifiedSessionPersistenceService", () => {
|
|
|
200
200
|
);
|
|
201
201
|
expect(row?.transcriptPath).toMatch(/\.log$/);
|
|
202
202
|
});
|
|
203
|
+
|
|
204
|
+
it("deletes the full root session directory even when artifact paths are stale", async () => {
|
|
205
|
+
const sessionsDir = mkdtempSync(join(tmpdir(), "delete-root-session-dir-"));
|
|
206
|
+
tempDirs.push(sessionsDir);
|
|
207
|
+
|
|
208
|
+
const store = new SqliteSessionStore({ sessionsDir });
|
|
209
|
+
stores.push(store);
|
|
210
|
+
const service = new CoreSessionService(store);
|
|
211
|
+
const sessionId = "root-session-delete";
|
|
212
|
+
const artifacts = await service.createRootSessionWithArtifacts({
|
|
213
|
+
sessionId,
|
|
214
|
+
source: SessionSource.CLI,
|
|
215
|
+
pid: process.pid,
|
|
216
|
+
interactive: false,
|
|
217
|
+
provider: "anthropic",
|
|
218
|
+
model: "claude-sonnet-4-6",
|
|
219
|
+
cwd: "/tmp/project",
|
|
220
|
+
workspaceRoot: "/tmp/project",
|
|
221
|
+
enableTools: true,
|
|
222
|
+
enableSpawn: false,
|
|
223
|
+
enableTeams: false,
|
|
224
|
+
prompt: "delete me",
|
|
225
|
+
startedAt: "2026-04-10T19:00:00.000Z",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
store.run(`UPDATE sessions SET messages_path = NULL WHERE session_id = ?`, [
|
|
229
|
+
sessionId,
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
expect(existsSync(artifacts.messagesPath)).toBe(true);
|
|
233
|
+
expect(existsSync(join(sessionsDir, sessionId))).toBe(true);
|
|
234
|
+
|
|
235
|
+
const result = await service.deleteSession(sessionId);
|
|
236
|
+
|
|
237
|
+
expect(result).toEqual({ deleted: true });
|
|
238
|
+
expect(existsSync(artifacts.messagesPath)).toBe(false);
|
|
239
|
+
expect(existsSync(join(sessionsDir, sessionId))).toBe(false);
|
|
240
|
+
});
|
|
203
241
|
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
readFileSync,
|
|
5
5
|
writeFileSync,
|
|
6
6
|
} from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
7
8
|
import type * as LlmsProviders from "@clinebot/llms";
|
|
8
9
|
import type { AgentResult } from "@clinebot/shared";
|
|
9
10
|
import { resolveRootSessionId } from "@clinebot/shared";
|
|
@@ -922,7 +923,21 @@ export class UnifiedSessionPersistenceService {
|
|
|
922
923
|
unlinkIfExists(row.hookPath);
|
|
923
924
|
unlinkIfExists(row.messagesPath);
|
|
924
925
|
unlinkIfExists(this.artifacts.sessionManifestPath(id, false));
|
|
925
|
-
|
|
926
|
+
if (row.isSubagent) {
|
|
927
|
+
this.artifacts.removeSessionDirIfEmpty(id);
|
|
928
|
+
} else {
|
|
929
|
+
const candidateDirs = new Set<string>([
|
|
930
|
+
this.artifacts.sessionArtifactsDir(id),
|
|
931
|
+
]);
|
|
932
|
+
for (const path of [row.transcriptPath, row.hookPath, row.messagesPath]) {
|
|
933
|
+
if (typeof path === "string" && path.trim().length > 0) {
|
|
934
|
+
candidateDirs.add(dirname(path));
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
for (const dir of candidateDirs) {
|
|
938
|
+
this.artifacts.removeDir(dir);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
926
941
|
return { deleted: true };
|
|
927
942
|
}
|
|
928
943
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
mkdirSync,
|
|
4
4
|
readdirSync,
|
|
5
5
|
rmdirSync,
|
|
6
|
+
rmSync,
|
|
6
7
|
unlinkSync,
|
|
7
8
|
} from "node:fs";
|
|
8
9
|
import { dirname, join } from "node:path";
|
|
@@ -116,6 +117,21 @@ export class SessionArtifacts {
|
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
public removeSessionDir(sessionId: string): void {
|
|
121
|
+
this.removeDir(this.sessionArtifactsDir(sessionId));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public removeDir(dir: string): void {
|
|
125
|
+
if (!existsSync(dir)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
rmSync(dir, { recursive: true, force: true });
|
|
130
|
+
} catch {
|
|
131
|
+
// Best-effort cleanup.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
public subagentArtifactPaths(
|
|
120
136
|
sessionId: string,
|
|
121
137
|
subAgentId: string,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ITelemetryService } from "@clinebot/shared";
|
|
2
2
|
import { resolveAndLoadAgentPlugins } from "../extensions/plugin/plugin-config-loader";
|
|
3
|
+
import type {
|
|
4
|
+
PluginInitializationFailure,
|
|
5
|
+
PluginInitializationWarning,
|
|
6
|
+
} from "../extensions/plugin/plugin-load-report";
|
|
3
7
|
import {
|
|
4
8
|
createHookAuditHooks,
|
|
5
9
|
createHookConfigFileHooks,
|
|
@@ -15,6 +19,43 @@ import {
|
|
|
15
19
|
import type { StartSessionInput } from "./session-manager";
|
|
16
20
|
import { hasRuntimeHooks, mergeAgentExtensions } from "./utils/helpers";
|
|
17
21
|
|
|
22
|
+
function formatPluginFailure(failure: PluginInitializationFailure): string {
|
|
23
|
+
const label = failure.pluginName ?? failure.pluginPath;
|
|
24
|
+
return `${label}: ${failure.message}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function logPluginDiagnostics(
|
|
28
|
+
failures: PluginInitializationFailure[],
|
|
29
|
+
warnings: PluginInitializationWarning[],
|
|
30
|
+
logger: CoreSessionConfig["logger"],
|
|
31
|
+
): void {
|
|
32
|
+
if (warnings.length > 0) {
|
|
33
|
+
for (const warning of warnings) {
|
|
34
|
+
logger?.log(warning.message, { severity: "warn" });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (failures.length === 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const preview = failures.slice(0, 3).map(formatPluginFailure).join("; ");
|
|
41
|
+
const suffix = failures.length > 3 ? `; and ${failures.length - 3} more` : "";
|
|
42
|
+
logger?.log(
|
|
43
|
+
`Some plugins failed to initialize. ${preview}${suffix}. Use --verbose for more details.`,
|
|
44
|
+
{ severity: "warn" },
|
|
45
|
+
);
|
|
46
|
+
for (const failure of failures) {
|
|
47
|
+
logger?.log(
|
|
48
|
+
`Plugin initialization failed (${failure.phase}) for ${failure.pluginPath}`,
|
|
49
|
+
{
|
|
50
|
+
severity: "warn",
|
|
51
|
+
stack: failure.stack,
|
|
52
|
+
pluginPath: failure.pluginPath,
|
|
53
|
+
pluginName: failure.pluginName,
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
18
59
|
export function resolveWorkspacePath(config: CoreSessionConfig): string {
|
|
19
60
|
return config.workspaceRoot ?? config.cwd;
|
|
20
61
|
}
|
|
@@ -61,6 +102,11 @@ export async function buildEffectiveConfig(
|
|
|
61
102
|
cwd: input.config.cwd,
|
|
62
103
|
onEvent: onPluginEvent,
|
|
63
104
|
});
|
|
105
|
+
logPluginDiagnostics(
|
|
106
|
+
loadedPlugins.failures,
|
|
107
|
+
loadedPlugins.warnings,
|
|
108
|
+
input.config.logger,
|
|
109
|
+
);
|
|
64
110
|
} catch (error) {
|
|
65
111
|
const message = error instanceof Error ? error.message : String(error);
|
|
66
112
|
input.config.logger?.log?.(
|
|
@@ -743,6 +743,110 @@ describe("createAgentTeamsTools runtime behavior", () => {
|
|
|
743
743
|
expect(awaitAllRuns?.timeoutMs).toBe(60 * 60 * 1000);
|
|
744
744
|
});
|
|
745
745
|
|
|
746
|
+
it("deduplicates concurrent sync team_run_task calls to the same agent", async () => {
|
|
747
|
+
let resolveRoute!: (value: { text: string; iterations: number }) => void;
|
|
748
|
+
const routePromise = new Promise<{ text: string; iterations: number }>(
|
|
749
|
+
(resolve) => {
|
|
750
|
+
resolveRoute = resolve;
|
|
751
|
+
},
|
|
752
|
+
);
|
|
753
|
+
const routeToTeammate = vi.fn(() => routePromise);
|
|
754
|
+
const runtime = {
|
|
755
|
+
routeToTeammate,
|
|
756
|
+
getMemberRole: vi.fn(() => "lead"),
|
|
757
|
+
} as unknown as AgentTeamsRuntime;
|
|
758
|
+
|
|
759
|
+
const tools = createAgentTeamsTools({
|
|
760
|
+
runtime,
|
|
761
|
+
requesterId: "lead",
|
|
762
|
+
teammateConfigProvider: makeTeammateConfigProvider(),
|
|
763
|
+
});
|
|
764
|
+
const runTask = tools.find((tool) => tool.name === "team_run_task");
|
|
765
|
+
expect(runTask).toBeDefined();
|
|
766
|
+
if (!runTask) {
|
|
767
|
+
throw new Error("Expected team_run_task tool to be defined");
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const ctx = { agentId: "lead", conversationId: "conv-1", iteration: 1 };
|
|
771
|
+
const input = {
|
|
772
|
+
agentId: "educator",
|
|
773
|
+
task: "Explain probability",
|
|
774
|
+
runMode: "sync",
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// Fire two concurrent sync calls to the same agent
|
|
778
|
+
const call1 = runTask.execute(input, ctx);
|
|
779
|
+
const call2 = runTask.execute(input, ctx);
|
|
780
|
+
|
|
781
|
+
// Second call should throw with duplicate detection error
|
|
782
|
+
await expect(call2).rejects.toThrow(
|
|
783
|
+
'Duplicate team_run_task call detected for agent "educator"',
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
// Only one routeToTeammate call should have been made
|
|
787
|
+
expect(routeToTeammate).toHaveBeenCalledTimes(1);
|
|
788
|
+
|
|
789
|
+
// Now resolve the first call
|
|
790
|
+
resolveRoute({ text: "Probability explained", iterations: 3 });
|
|
791
|
+
const result1 = (await call1) as { text?: string; iterations?: number };
|
|
792
|
+
expect(result1.text).toBe("Probability explained");
|
|
793
|
+
expect(result1.iterations).toBe(3);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it("allows concurrent sync team_run_task calls to different agents", async () => {
|
|
797
|
+
let resolveRoute1!: (value: { text: string; iterations: number }) => void;
|
|
798
|
+
let resolveRoute2!: (value: { text: string; iterations: number }) => void;
|
|
799
|
+
const routeToTeammate = vi.fn((agentId: string) => {
|
|
800
|
+
if (agentId === "educator") {
|
|
801
|
+
return new Promise<{ text: string; iterations: number }>((resolve) => {
|
|
802
|
+
resolveRoute1 = resolve;
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return new Promise<{ text: string; iterations: number }>((resolve) => {
|
|
806
|
+
resolveRoute2 = resolve;
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
const runtime = {
|
|
810
|
+
routeToTeammate,
|
|
811
|
+
getMemberRole: vi.fn(() => "lead"),
|
|
812
|
+
} as unknown as AgentTeamsRuntime;
|
|
813
|
+
|
|
814
|
+
const tools = createAgentTeamsTools({
|
|
815
|
+
runtime,
|
|
816
|
+
requesterId: "lead",
|
|
817
|
+
teammateConfigProvider: makeTeammateConfigProvider(),
|
|
818
|
+
});
|
|
819
|
+
const runTask = tools.find((tool) => tool.name === "team_run_task");
|
|
820
|
+
expect(runTask).toBeDefined();
|
|
821
|
+
if (!runTask) {
|
|
822
|
+
throw new Error("Expected team_run_task tool to be defined");
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const ctx = { agentId: "lead", conversationId: "conv-1", iteration: 1 };
|
|
826
|
+
|
|
827
|
+
// Fire sync calls to two different agents - both should proceed
|
|
828
|
+
const call1 = runTask.execute(
|
|
829
|
+
{ agentId: "educator", task: "Explain probability", runMode: "sync" },
|
|
830
|
+
ctx,
|
|
831
|
+
);
|
|
832
|
+
const call2 = runTask.execute(
|
|
833
|
+
{ agentId: "assessor", task: "Evaluate answer", runMode: "sync" },
|
|
834
|
+
ctx,
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
// Both should have called routeToTeammate
|
|
838
|
+
expect(routeToTeammate).toHaveBeenCalledTimes(2);
|
|
839
|
+
|
|
840
|
+
// Resolve both
|
|
841
|
+
resolveRoute1({ text: "Explained", iterations: 2 });
|
|
842
|
+
resolveRoute2({ text: "Evaluated", iterations: 1 });
|
|
843
|
+
|
|
844
|
+
const result1 = (await call1) as { text?: string };
|
|
845
|
+
const result2 = (await call2) as { text?: string };
|
|
846
|
+
expect(result1.text).toBe("Explained");
|
|
847
|
+
expect(result2.text).toBe("Evaluated");
|
|
848
|
+
});
|
|
849
|
+
|
|
746
850
|
it("lists ready-to-claim tasks through team_task list action", async () => {
|
|
747
851
|
const runtime = new AgentTeamsRuntime({ teamName: "test-team" });
|
|
748
852
|
const tools = createAgentTeamsTools({
|
package/src/team/team-tools.ts
CHANGED
|
@@ -383,6 +383,11 @@ export function createAgentTeamsTools(
|
|
|
383
383
|
}) as Tool,
|
|
384
384
|
);
|
|
385
385
|
|
|
386
|
+
// Track in-flight sync runs per agent for dedup
|
|
387
|
+
// (Claude sometimes emits duplicate tool_use blocks in a single response;
|
|
388
|
+
// we execute the first and return an informative "duplicate ignored" to the rest)
|
|
389
|
+
const pendingSyncRuns = new Set<string>();
|
|
390
|
+
|
|
386
391
|
tools.push(
|
|
387
392
|
createTool<
|
|
388
393
|
TeamRunTaskInput,
|
|
@@ -417,22 +422,36 @@ export function createAgentTeamsTools(
|
|
|
417
422
|
runId: run.id,
|
|
418
423
|
};
|
|
419
424
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
425
|
+
|
|
426
|
+
/// Deduplication guard: reject a duplicate sync call for the same agent
|
|
427
|
+
// that was already dispatched in this same parallel tool-call batch.
|
|
428
|
+
if (pendingSyncRuns.has(validatedInput.agentId)) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`Duplicate team_run_task call detected for agent "${validatedInput.agentId}". ` +
|
|
431
|
+
`Only one call per agent is allowed per turn. Discard this duplicate result.`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
pendingSyncRuns.add(validatedInput.agentId);
|
|
435
|
+
try {
|
|
436
|
+
const result = await options.runtime.routeToTeammate(
|
|
437
|
+
validatedInput.agentId,
|
|
438
|
+
validatedInput.task,
|
|
439
|
+
{
|
|
440
|
+
taskId: validatedInput.taskId || undefined,
|
|
441
|
+
fromAgentId: options.requesterId,
|
|
442
|
+
continueConversation:
|
|
443
|
+
validatedInput.continueConversation || undefined,
|
|
444
|
+
},
|
|
445
|
+
);
|
|
446
|
+
return {
|
|
447
|
+
agentId: validatedInput.agentId,
|
|
448
|
+
mode: "sync",
|
|
449
|
+
text: result.text,
|
|
450
|
+
iterations: result.iterations,
|
|
451
|
+
};
|
|
452
|
+
} finally {
|
|
453
|
+
pendingSyncRuns.delete(validatedInput.agentId);
|
|
454
|
+
}
|
|
436
455
|
},
|
|
437
456
|
}) as Tool,
|
|
438
457
|
);
|
package/src/types/config.ts
CHANGED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { AgentTeamsRuntime, DelegatedAgentConfigProvider } from "../team";
|
|
2
|
-
export interface TeamRuntimeRegistryEntry {
|
|
3
|
-
runtime?: AgentTeamsRuntime;
|
|
4
|
-
delegatedAgentConfigProvider: DelegatedAgentConfigProvider;
|
|
5
|
-
}
|
|
6
|
-
export declare class TeamRuntimeRegistry {
|
|
7
|
-
private readonly entries;
|
|
8
|
-
get(key: string): TeamRuntimeRegistryEntry | undefined;
|
|
9
|
-
getOrCreate(key: string, create: () => TeamRuntimeRegistryEntry): TeamRuntimeRegistryEntry;
|
|
10
|
-
update(key: string, updateEntry: (entry: TeamRuntimeRegistryEntry) => void): TeamRuntimeRegistryEntry | undefined;
|
|
11
|
-
delete(key: string): void;
|
|
12
|
-
}
|
|
13
|
-
//# sourceMappingURL=team-runtime-registry.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"team-runtime-registry.d.ts","sourceRoot":"","sources":["../../src/runtime/team-runtime-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AAE/E,MAAM,WAAW,wBAAwB;IACxC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,4BAA4B,EAAE,4BAA4B,CAAC;CAC3D;AAED,qBAAa,mBAAmB;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+C;IAEvE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,wBAAwB,GAAG,SAAS;IAItD,WAAW,CACV,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,wBAAwB,GACpC,wBAAwB;IAU3B,MAAM,CACL,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,CAAC,KAAK,EAAE,wBAAwB,KAAK,IAAI,GACpD,wBAAwB,GAAG,SAAS;IASvC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAGzB"}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { AgentTeamsRuntime, DelegatedAgentConfigProvider } from "../team";
|
|
2
|
-
|
|
3
|
-
export interface TeamRuntimeRegistryEntry {
|
|
4
|
-
runtime?: AgentTeamsRuntime;
|
|
5
|
-
delegatedAgentConfigProvider: DelegatedAgentConfigProvider;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class TeamRuntimeRegistry {
|
|
9
|
-
private readonly entries = new Map<string, TeamRuntimeRegistryEntry>();
|
|
10
|
-
|
|
11
|
-
get(key: string): TeamRuntimeRegistryEntry | undefined {
|
|
12
|
-
return this.entries.get(key);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
getOrCreate(
|
|
16
|
-
key: string,
|
|
17
|
-
create: () => TeamRuntimeRegistryEntry,
|
|
18
|
-
): TeamRuntimeRegistryEntry {
|
|
19
|
-
const existing = this.entries.get(key);
|
|
20
|
-
if (existing) {
|
|
21
|
-
return existing;
|
|
22
|
-
}
|
|
23
|
-
const created = create();
|
|
24
|
-
this.entries.set(key, created);
|
|
25
|
-
return created;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
update(
|
|
29
|
-
key: string,
|
|
30
|
-
updateEntry: (entry: TeamRuntimeRegistryEntry) => void,
|
|
31
|
-
): TeamRuntimeRegistryEntry | undefined {
|
|
32
|
-
const entry = this.entries.get(key);
|
|
33
|
-
if (!entry) {
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
updateEntry(entry);
|
|
37
|
-
return entry;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
delete(key: string): void {
|
|
41
|
-
this.entries.delete(key);
|
|
42
|
-
}
|
|
43
|
-
}
|