@clinebot/core 0.0.10 → 0.0.12
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/agents/agent-config-loader.d.ts +1 -1
- package/dist/agents/agent-config-parser.d.ts +5 -2
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/plugin-config-loader.d.ts +4 -0
- package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
- package/dist/agents/plugin-sandbox.d.ts +4 -0
- package/dist/index.node.d.ts +1 -1
- package/dist/index.node.js +658 -407
- package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
- package/dist/session/default-session-manager.d.ts +5 -0
- package/dist/session/session-config-builder.d.ts +4 -1
- package/dist/session/session-manager.d.ts +1 -0
- package/dist/session/unified-session-persistence-service.d.ts +6 -0
- package/dist/session/utils/helpers.d.ts +1 -1
- package/dist/session/utils/types.d.ts +10 -0
- package/dist/tools/definitions.d.ts +2 -2
- package/dist/tools/presets.d.ts +3 -3
- package/dist/tools/schemas.d.ts +14 -14
- package/dist/types/config.d.ts +5 -0
- package/dist/types/events.d.ts +22 -0
- package/package.json +5 -4
- package/src/agents/agent-config-loader.test.ts +2 -0
- package/src/agents/agent-config-loader.ts +1 -0
- package/src/agents/agent-config-parser.ts +12 -5
- package/src/agents/index.ts +1 -0
- package/src/agents/plugin-config-loader.test.ts +49 -0
- package/src/agents/plugin-config-loader.ts +10 -73
- package/src/agents/plugin-loader.test.ts +128 -2
- package/src/agents/plugin-loader.ts +70 -5
- package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
- package/src/agents/plugin-sandbox.test.ts +198 -1
- package/src/agents/plugin-sandbox.ts +223 -353
- package/src/index.node.ts +4 -0
- package/src/runtime/hook-file-hooks.test.ts +1 -1
- package/src/runtime/hook-file-hooks.ts +16 -6
- package/src/runtime/runtime-builder.test.ts +67 -0
- package/src/runtime/runtime-builder.ts +70 -16
- package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
- package/src/session/default-session-manager.e2e.test.ts +20 -1
- package/src/session/default-session-manager.test.ts +584 -1
- package/src/session/default-session-manager.ts +205 -1
- package/src/session/session-config-builder.ts +2 -0
- package/src/session/session-manager.ts +1 -0
- package/src/session/session-team-coordination.ts +30 -0
- package/src/session/unified-session-persistence-service.ts +45 -0
- package/src/session/utils/helpers.ts +13 -3
- package/src/session/utils/types.ts +11 -0
- package/src/storage/sqlite-team-store.ts +16 -5
- package/src/tools/definitions.test.ts +87 -8
- package/src/tools/definitions.ts +89 -70
- package/src/tools/presets.test.ts +2 -3
- package/src/tools/presets.ts +3 -3
- package/src/tools/schemas.ts +23 -22
- package/src/types/config.ts +5 -0
- package/src/types/events.ts +23 -0
|
@@ -223,6 +223,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
223
223
|
hookPath,
|
|
224
224
|
sessionId,
|
|
225
225
|
this.defaultTelemetry,
|
|
226
|
+
(e) => void this.handlePluginEvent(sessionId, e),
|
|
226
227
|
);
|
|
227
228
|
const providerConfig = buildResolvedProviderConfig(
|
|
228
229
|
effectiveConfig,
|
|
@@ -303,9 +304,12 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
303
304
|
started: false,
|
|
304
305
|
aborting: false,
|
|
305
306
|
interactive: input.interactive === true,
|
|
307
|
+
persistedMessages: input.initialMessages,
|
|
306
308
|
activeTeamRunIds: new Set<string>(),
|
|
307
309
|
pendingTeamRunUpdates: [],
|
|
308
310
|
teamRunWaiters: [],
|
|
311
|
+
pendingPrompts: [],
|
|
312
|
+
drainingPendingPrompts: false,
|
|
309
313
|
pluginSandboxShutdown,
|
|
310
314
|
};
|
|
311
315
|
this.sessions.set(sessionId, active);
|
|
@@ -348,8 +352,18 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
348
352
|
promptLength: input.prompt.length,
|
|
349
353
|
userImageCount: input.userImages?.length ?? 0,
|
|
350
354
|
userFileCount: input.userFiles?.length ?? 0,
|
|
355
|
+
delivery: input.delivery ?? "immediate",
|
|
351
356
|
},
|
|
352
357
|
});
|
|
358
|
+
if (input.delivery === "queue" || input.delivery === "steer") {
|
|
359
|
+
this.enqueuePendingPrompt(input.sessionId, {
|
|
360
|
+
prompt: input.prompt,
|
|
361
|
+
delivery: input.delivery,
|
|
362
|
+
userImages: input.userImages,
|
|
363
|
+
userFiles: input.userFiles,
|
|
364
|
+
});
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
353
367
|
try {
|
|
354
368
|
const result = await this.runTurn(session, {
|
|
355
369
|
prompt: input.prompt,
|
|
@@ -359,6 +373,9 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
359
373
|
if (!session.interactive) {
|
|
360
374
|
await this.finalizeSingleRun(session, result.finishReason);
|
|
361
375
|
}
|
|
376
|
+
queueMicrotask(() => {
|
|
377
|
+
void this.drainPendingPrompts(input.sessionId);
|
|
378
|
+
});
|
|
362
379
|
return result;
|
|
363
380
|
} catch (error) {
|
|
364
381
|
await this.failSession(session);
|
|
@@ -548,7 +565,8 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
548
565
|
): Promise<AgentResult> {
|
|
549
566
|
const shouldContinue =
|
|
550
567
|
session.started || session.agent.getMessages().length > 0;
|
|
551
|
-
const baselineMessages =
|
|
568
|
+
const baselineMessages =
|
|
569
|
+
session.persistedMessages ?? session.agent.getMessages();
|
|
552
570
|
const usageBaseline =
|
|
553
571
|
this.usageBySession.get(session.sessionId) ??
|
|
554
572
|
createInitialAccumulatedUsage();
|
|
@@ -582,7 +600,9 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
582
600
|
const persistedMessages = withLatestAssistantTurnMetadata(
|
|
583
601
|
result.messages,
|
|
584
602
|
result,
|
|
603
|
+
baselineMessages,
|
|
585
604
|
);
|
|
605
|
+
session.persistedMessages = persistedMessages;
|
|
586
606
|
this.usageBySession.set(
|
|
587
607
|
session.sessionId,
|
|
588
608
|
accumulateUsageTotals(usageBaseline, result.usage),
|
|
@@ -762,6 +782,151 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
762
782
|
this.emitStatus(session.sessionId, status);
|
|
763
783
|
}
|
|
764
784
|
|
|
785
|
+
private async handlePluginEvent(
|
|
786
|
+
rootSessionId: string,
|
|
787
|
+
event: { name: string; payload?: unknown },
|
|
788
|
+
): Promise<void> {
|
|
789
|
+
if (
|
|
790
|
+
event.name !== "steer_message" &&
|
|
791
|
+
event.name !== "queue_message" &&
|
|
792
|
+
event.name !== "pending_prompt"
|
|
793
|
+
) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const payload =
|
|
797
|
+
event.payload && typeof event.payload === "object"
|
|
798
|
+
? (event.payload as Record<string, unknown>)
|
|
799
|
+
: undefined;
|
|
800
|
+
const targetSessionId =
|
|
801
|
+
typeof payload?.sessionId === "string" &&
|
|
802
|
+
payload.sessionId.trim().length > 0
|
|
803
|
+
? payload.sessionId.trim()
|
|
804
|
+
: rootSessionId;
|
|
805
|
+
const prompt =
|
|
806
|
+
typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
|
|
807
|
+
if (!prompt) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const delivery =
|
|
811
|
+
event.name === "steer_message"
|
|
812
|
+
? "steer"
|
|
813
|
+
: event.name === "queue_message"
|
|
814
|
+
? "queue"
|
|
815
|
+
: payload?.delivery === "steer"
|
|
816
|
+
? "steer"
|
|
817
|
+
: "queue";
|
|
818
|
+
this.enqueuePendingPrompt(targetSessionId, {
|
|
819
|
+
prompt,
|
|
820
|
+
delivery,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private enqueuePendingPrompt(
|
|
825
|
+
sessionId: string,
|
|
826
|
+
entry: {
|
|
827
|
+
prompt: string;
|
|
828
|
+
delivery: "queue" | "steer";
|
|
829
|
+
userImages?: string[];
|
|
830
|
+
userFiles?: string[];
|
|
831
|
+
},
|
|
832
|
+
): void {
|
|
833
|
+
const session = this.sessions.get(sessionId);
|
|
834
|
+
if (!session) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const { prompt, delivery, userImages, userFiles } = entry;
|
|
838
|
+
const existingIndex = session.pendingPrompts.findIndex(
|
|
839
|
+
(queued) => queued.prompt === prompt,
|
|
840
|
+
);
|
|
841
|
+
if (existingIndex >= 0) {
|
|
842
|
+
const [existing] = session.pendingPrompts.splice(existingIndex, 1);
|
|
843
|
+
if (delivery === "steer" || existing.delivery === "steer") {
|
|
844
|
+
session.pendingPrompts.unshift({
|
|
845
|
+
id: existing.id,
|
|
846
|
+
prompt,
|
|
847
|
+
delivery: "steer",
|
|
848
|
+
userImages: userImages ?? existing.userImages,
|
|
849
|
+
userFiles: userFiles ?? existing.userFiles,
|
|
850
|
+
});
|
|
851
|
+
} else {
|
|
852
|
+
session.pendingPrompts.push({
|
|
853
|
+
...existing,
|
|
854
|
+
userImages: userImages ?? existing.userImages,
|
|
855
|
+
userFiles: userFiles ?? existing.userFiles,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
} else if (delivery === "steer") {
|
|
859
|
+
session.pendingPrompts.unshift({
|
|
860
|
+
id: `pending_${Date.now()}_${nanoid(5)}`,
|
|
861
|
+
prompt,
|
|
862
|
+
delivery,
|
|
863
|
+
userImages,
|
|
864
|
+
userFiles,
|
|
865
|
+
});
|
|
866
|
+
} else {
|
|
867
|
+
session.pendingPrompts.push({
|
|
868
|
+
id: `pending_${Date.now()}_${nanoid(5)}`,
|
|
869
|
+
prompt,
|
|
870
|
+
delivery,
|
|
871
|
+
userImages,
|
|
872
|
+
userFiles,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
this.emitPendingPrompts(session);
|
|
876
|
+
queueMicrotask(() => {
|
|
877
|
+
void this.drainPendingPrompts(sessionId);
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
private async drainPendingPrompts(sessionId: string): Promise<void> {
|
|
882
|
+
const session = this.sessions.get(sessionId);
|
|
883
|
+
if (!session || session.drainingPendingPrompts) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const canStartRun =
|
|
887
|
+
typeof (session.agent as Agent & { canStartRun?: () => boolean })
|
|
888
|
+
.canStartRun === "function"
|
|
889
|
+
? (
|
|
890
|
+
session.agent as Agent & {
|
|
891
|
+
canStartRun: () => boolean;
|
|
892
|
+
}
|
|
893
|
+
).canStartRun()
|
|
894
|
+
: true;
|
|
895
|
+
if (!canStartRun) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const next = session.pendingPrompts.shift();
|
|
899
|
+
if (!next) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
this.emitPendingPrompts(session);
|
|
903
|
+
this.emitPendingPromptSubmitted(session, next);
|
|
904
|
+
session.drainingPendingPrompts = true;
|
|
905
|
+
try {
|
|
906
|
+
await this.send({
|
|
907
|
+
sessionId,
|
|
908
|
+
prompt: next.prompt,
|
|
909
|
+
userImages: next.userImages,
|
|
910
|
+
userFiles: next.userFiles,
|
|
911
|
+
});
|
|
912
|
+
} catch (error) {
|
|
913
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
914
|
+
if (message.includes("already in progress")) {
|
|
915
|
+
session.pendingPrompts.unshift(next);
|
|
916
|
+
this.emitPendingPrompts(session);
|
|
917
|
+
} else {
|
|
918
|
+
throw error;
|
|
919
|
+
}
|
|
920
|
+
} finally {
|
|
921
|
+
session.drainingPendingPrompts = false;
|
|
922
|
+
if (session.pendingPrompts.length > 0) {
|
|
923
|
+
queueMicrotask(() => {
|
|
924
|
+
void this.drainPendingPrompts(sessionId);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
765
930
|
// ── Agent event handling ────────────────────────────────────────────
|
|
766
931
|
|
|
767
932
|
private onAgentEvent(
|
|
@@ -787,6 +952,45 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
787
952
|
handleAgentEvent(ctx, event);
|
|
788
953
|
}
|
|
789
954
|
|
|
955
|
+
private emitPendingPrompts(session: ActiveSession): void {
|
|
956
|
+
this.emit({
|
|
957
|
+
type: "pending_prompts",
|
|
958
|
+
payload: {
|
|
959
|
+
sessionId: session.sessionId,
|
|
960
|
+
prompts: session.pendingPrompts.map((entry) => ({
|
|
961
|
+
id: entry.id,
|
|
962
|
+
prompt: entry.prompt,
|
|
963
|
+
delivery: entry.delivery,
|
|
964
|
+
attachmentCount:
|
|
965
|
+
(entry.userImages?.length ?? 0) + (entry.userFiles?.length ?? 0),
|
|
966
|
+
})),
|
|
967
|
+
},
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private emitPendingPromptSubmitted(
|
|
972
|
+
session: ActiveSession,
|
|
973
|
+
entry: {
|
|
974
|
+
id: string;
|
|
975
|
+
prompt: string;
|
|
976
|
+
delivery: "queue" | "steer";
|
|
977
|
+
userImages?: string[];
|
|
978
|
+
userFiles?: string[];
|
|
979
|
+
},
|
|
980
|
+
): void {
|
|
981
|
+
this.emit({
|
|
982
|
+
type: "pending_prompt_submitted",
|
|
983
|
+
payload: {
|
|
984
|
+
sessionId: session.sessionId,
|
|
985
|
+
id: entry.id,
|
|
986
|
+
prompt: entry.prompt,
|
|
987
|
+
delivery: entry.delivery,
|
|
988
|
+
attachmentCount:
|
|
989
|
+
(entry.userImages?.length ?? 0) + (entry.userFiles?.length ?? 0),
|
|
990
|
+
},
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
790
994
|
// ── Spawn / sub-agents ──────────────────────────────────────────────
|
|
791
995
|
|
|
792
996
|
private createSpawnTool(
|
|
@@ -24,6 +24,7 @@ export async function buildEffectiveConfig(
|
|
|
24
24
|
hookPath: string,
|
|
25
25
|
sessionId: string,
|
|
26
26
|
defaultTelemetry: ITelemetryService | undefined,
|
|
27
|
+
onPluginEvent?: (event: { name: string; payload?: unknown }) => void,
|
|
27
28
|
): Promise<{
|
|
28
29
|
config: CoreSessionConfig;
|
|
29
30
|
pluginSandboxShutdown?: () => Promise<void>;
|
|
@@ -54,6 +55,7 @@ export async function buildEffectiveConfig(
|
|
|
54
55
|
pluginPaths: input.config.pluginPaths,
|
|
55
56
|
workspacePath,
|
|
56
57
|
cwd: input.config.cwd,
|
|
58
|
+
onEvent: onPluginEvent,
|
|
57
59
|
});
|
|
58
60
|
const effectiveExtensions = mergeAgentExtensions(
|
|
59
61
|
input.config.extensions,
|
|
@@ -52,6 +52,36 @@ export async function dispatchTeamEventToBackend(
|
|
|
52
52
|
invokeOptional: (method: string, ...args: unknown[]) => Promise<void>,
|
|
53
53
|
): Promise<void> {
|
|
54
54
|
switch (event.type) {
|
|
55
|
+
case "run_progress":
|
|
56
|
+
await invokeOptional(
|
|
57
|
+
"onTeamTaskProgress",
|
|
58
|
+
rootSessionId,
|
|
59
|
+
event.run.agentId,
|
|
60
|
+
event.message,
|
|
61
|
+
{ kind: event.message === "heartbeat" ? "heartbeat" : "progress" },
|
|
62
|
+
);
|
|
63
|
+
break;
|
|
64
|
+
case "agent_event":
|
|
65
|
+
if (
|
|
66
|
+
event.event.type === "content_start" &&
|
|
67
|
+
event.event.contentType === "text" &&
|
|
68
|
+
typeof event.event.text === "string"
|
|
69
|
+
) {
|
|
70
|
+
const snippet = event.event.text
|
|
71
|
+
.replace(/\s+/g, " ")
|
|
72
|
+
.trim()
|
|
73
|
+
.slice(0, 120);
|
|
74
|
+
if (snippet) {
|
|
75
|
+
await invokeOptional(
|
|
76
|
+
"onTeamTaskProgress",
|
|
77
|
+
rootSessionId,
|
|
78
|
+
event.agentId,
|
|
79
|
+
snippet,
|
|
80
|
+
{ kind: "text" },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
55
85
|
case "task_start":
|
|
56
86
|
await invokeOptional(
|
|
57
87
|
"onTeamTaskStart",
|
|
@@ -167,9 +167,15 @@ export interface SessionPersistenceAdapter {
|
|
|
167
167
|
|
|
168
168
|
export class UnifiedSessionPersistenceService {
|
|
169
169
|
private readonly teamTaskSessionsByAgent = new Map<string, string[]>();
|
|
170
|
+
private readonly teamTaskLastHeartbeatBySession = new Map<string, number>();
|
|
171
|
+
private readonly teamTaskLastProgressLineBySession = new Map<
|
|
172
|
+
string,
|
|
173
|
+
string
|
|
174
|
+
>();
|
|
170
175
|
protected readonly artifacts: SessionArtifacts;
|
|
171
176
|
private static readonly STALE_REASON = "failed_external_process_exit";
|
|
172
177
|
private static readonly STALE_SOURCE = "stale_session_reconciler";
|
|
178
|
+
private static readonly TEAM_HEARTBEAT_LOG_INTERVAL_MS = 30_000;
|
|
173
179
|
|
|
174
180
|
constructor(private readonly adapter: SessionPersistenceAdapter) {
|
|
175
181
|
this.artifacts = new SessionArtifacts(() => this.ensureSessionsDir());
|
|
@@ -763,6 +769,45 @@ export class UnifiedSessionPersistenceService {
|
|
|
763
769
|
summary ?? `[done] ${status}`,
|
|
764
770
|
);
|
|
765
771
|
await this.applySubagentStatusBySessionId(sessionId, status);
|
|
772
|
+
this.teamTaskLastHeartbeatBySession.delete(sessionId);
|
|
773
|
+
this.teamTaskLastProgressLineBySession.delete(sessionId);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async onTeamTaskProgress(
|
|
777
|
+
rootSessionId: string,
|
|
778
|
+
agentId: string,
|
|
779
|
+
progress: string,
|
|
780
|
+
options?: { kind?: "heartbeat" | "progress" | "text" },
|
|
781
|
+
): Promise<void> {
|
|
782
|
+
const key = this.teamTaskQueueKey(rootSessionId, agentId);
|
|
783
|
+
const sessionId = this.teamTaskSessionsByAgent.get(key)?.[0];
|
|
784
|
+
if (!sessionId) return;
|
|
785
|
+
|
|
786
|
+
const trimmed = progress.trim();
|
|
787
|
+
if (!trimmed) return;
|
|
788
|
+
|
|
789
|
+
const kind = options?.kind ?? "progress";
|
|
790
|
+
if (kind === "heartbeat") {
|
|
791
|
+
const now = Date.now();
|
|
792
|
+
const last = this.teamTaskLastHeartbeatBySession.get(sessionId) ?? 0;
|
|
793
|
+
if (
|
|
794
|
+
now - last <
|
|
795
|
+
UnifiedSessionPersistenceService.TEAM_HEARTBEAT_LOG_INTERVAL_MS
|
|
796
|
+
) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
this.teamTaskLastHeartbeatBySession.set(sessionId, now);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const line =
|
|
803
|
+
kind === "heartbeat"
|
|
804
|
+
? "[progress] heartbeat"
|
|
805
|
+
: kind === "text"
|
|
806
|
+
? `[progress] text: ${trimmed}`
|
|
807
|
+
: `[progress] ${trimmed}`;
|
|
808
|
+
if (this.teamTaskLastProgressLineBySession.get(sessionId) === line) return;
|
|
809
|
+
this.teamTaskLastProgressLineBySession.set(sessionId, line);
|
|
810
|
+
await this.appendSubagentTranscriptLine(sessionId, line);
|
|
766
811
|
}
|
|
767
812
|
|
|
768
813
|
// ── SubAgent lifecycle ────────────────────────────────────────────
|
|
@@ -62,10 +62,20 @@ export function serializeAgentEvent(event: AgentEvent): string {
|
|
|
62
62
|
export function withLatestAssistantTurnMetadata(
|
|
63
63
|
messages: LlmsProviders.Message[],
|
|
64
64
|
result: AgentResult,
|
|
65
|
+
previousMessages: LlmsProviders.MessageWithMetadata[] = [],
|
|
65
66
|
): StoredMessageWithMetadata[] {
|
|
66
|
-
const next = messages.map((message) =>
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const next = messages.map((message, index) => {
|
|
68
|
+
const previous = previousMessages[index];
|
|
69
|
+
const sameMessage =
|
|
70
|
+
previous?.role === message.role &&
|
|
71
|
+
JSON.stringify(previous.content) === JSON.stringify(message.content);
|
|
72
|
+
return sameMessage
|
|
73
|
+
? ({
|
|
74
|
+
...previous,
|
|
75
|
+
...message,
|
|
76
|
+
} as StoredMessageWithMetadata)
|
|
77
|
+
: ({ ...message } as StoredMessageWithMetadata);
|
|
78
|
+
});
|
|
69
79
|
const assistantIndex = [...next]
|
|
70
80
|
.reverse()
|
|
71
81
|
.findIndex((message) => message.role === "assistant");
|
|
@@ -18,13 +18,24 @@ export type ActiveSession = {
|
|
|
18
18
|
started: boolean;
|
|
19
19
|
aborting: boolean;
|
|
20
20
|
interactive: boolean;
|
|
21
|
+
persistedMessages?: LlmsProviders.MessageWithMetadata[];
|
|
21
22
|
activeTeamRunIds: Set<string>;
|
|
22
23
|
pendingTeamRunUpdates: TeamRunUpdate[];
|
|
23
24
|
teamRunWaiters: Array<() => void>;
|
|
25
|
+
pendingPrompts: PendingPrompt[];
|
|
26
|
+
drainingPendingPrompts: boolean;
|
|
24
27
|
pluginSandboxShutdown?: () => Promise<void>;
|
|
25
28
|
turnUsageBaseline?: SessionAccumulatedUsage;
|
|
26
29
|
};
|
|
27
30
|
|
|
31
|
+
export type PendingPrompt = {
|
|
32
|
+
id: string;
|
|
33
|
+
prompt: string;
|
|
34
|
+
delivery: "queue" | "steer";
|
|
35
|
+
userImages?: string[];
|
|
36
|
+
userFiles?: string[];
|
|
37
|
+
};
|
|
38
|
+
|
|
28
39
|
export type TeamRunUpdate = {
|
|
29
40
|
runId: string;
|
|
30
41
|
agentId: string;
|
|
@@ -120,6 +120,8 @@ export class SqliteTeamStore implements TeamStore {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
private ensureSchema(db: SqliteDb): void {
|
|
123
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
124
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
123
125
|
db.exec(`
|
|
124
126
|
CREATE TABLE IF NOT EXISTS team_events (
|
|
125
127
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -130,16 +132,20 @@ export class SqliteTeamStore implements TeamStore {
|
|
|
130
132
|
causation_id TEXT,
|
|
131
133
|
correlation_id TEXT
|
|
132
134
|
);
|
|
135
|
+
`);
|
|
136
|
+
db.exec(`
|
|
133
137
|
CREATE INDEX IF NOT EXISTS idx_team_events_name_ts
|
|
134
138
|
ON team_events(team_name, ts DESC);
|
|
135
|
-
|
|
139
|
+
`);
|
|
140
|
+
db.exec(`
|
|
136
141
|
CREATE TABLE IF NOT EXISTS team_runtime_snapshot (
|
|
137
142
|
team_name TEXT PRIMARY KEY,
|
|
138
143
|
state_json TEXT NOT NULL,
|
|
139
144
|
teammates_json TEXT NOT NULL,
|
|
140
145
|
updated_at TEXT NOT NULL
|
|
141
146
|
);
|
|
142
|
-
|
|
147
|
+
`);
|
|
148
|
+
db.exec(`
|
|
143
149
|
CREATE TABLE IF NOT EXISTS team_tasks (
|
|
144
150
|
team_name TEXT NOT NULL,
|
|
145
151
|
task_id TEXT NOT NULL,
|
|
@@ -153,7 +159,8 @@ export class SqliteTeamStore implements TeamStore {
|
|
|
153
159
|
updated_at TEXT NOT NULL,
|
|
154
160
|
PRIMARY KEY(team_name, task_id)
|
|
155
161
|
);
|
|
156
|
-
|
|
162
|
+
`);
|
|
163
|
+
db.exec(`
|
|
157
164
|
CREATE TABLE IF NOT EXISTS team_runs (
|
|
158
165
|
team_name TEXT NOT NULL,
|
|
159
166
|
run_id TEXT NOT NULL,
|
|
@@ -169,9 +176,12 @@ export class SqliteTeamStore implements TeamStore {
|
|
|
169
176
|
version INTEGER NOT NULL DEFAULT 1,
|
|
170
177
|
PRIMARY KEY(team_name, run_id)
|
|
171
178
|
);
|
|
179
|
+
`);
|
|
180
|
+
db.exec(`
|
|
172
181
|
CREATE INDEX IF NOT EXISTS idx_team_runs_status
|
|
173
182
|
ON team_runs(team_name, status);
|
|
174
|
-
|
|
183
|
+
`);
|
|
184
|
+
db.exec(`
|
|
175
185
|
CREATE TABLE IF NOT EXISTS team_outcomes (
|
|
176
186
|
team_name TEXT NOT NULL,
|
|
177
187
|
outcome_id TEXT NOT NULL,
|
|
@@ -182,7 +192,8 @@ export class SqliteTeamStore implements TeamStore {
|
|
|
182
192
|
version INTEGER NOT NULL DEFAULT 1,
|
|
183
193
|
PRIMARY KEY(team_name, outcome_id)
|
|
184
194
|
);
|
|
185
|
-
|
|
195
|
+
`);
|
|
196
|
+
db.exec(`
|
|
186
197
|
CREATE TABLE IF NOT EXISTS team_outcome_fragments (
|
|
187
198
|
team_name TEXT NOT NULL,
|
|
188
199
|
outcome_id TEXT NOT NULL,
|
|
@@ -3,7 +3,18 @@ import {
|
|
|
3
3
|
createBashTool,
|
|
4
4
|
createDefaultTools,
|
|
5
5
|
createReadFilesTool,
|
|
6
|
+
createSkillsTool,
|
|
6
7
|
} from "./definitions.js";
|
|
8
|
+
import type { SkillsExecutorWithMetadata } from "./types.js";
|
|
9
|
+
|
|
10
|
+
function createMockSkillsExecutor(
|
|
11
|
+
fn: (...args: unknown[]) => Promise<string> = async () => "ok",
|
|
12
|
+
configuredSkills?: SkillsExecutorWithMetadata["configuredSkills"],
|
|
13
|
+
): SkillsExecutorWithMetadata {
|
|
14
|
+
const executor = fn as SkillsExecutorWithMetadata;
|
|
15
|
+
executor.configuredSkills = configuredSkills;
|
|
16
|
+
return executor;
|
|
17
|
+
}
|
|
7
18
|
|
|
8
19
|
describe("default skills tool", () => {
|
|
9
20
|
it("is included only when enabled with a skills executor", () => {
|
|
@@ -17,18 +28,43 @@ describe("default skills tool", () => {
|
|
|
17
28
|
|
|
18
29
|
const toolsWithExecutor = createDefaultTools({
|
|
19
30
|
executors: {
|
|
20
|
-
skills:
|
|
31
|
+
skills: createMockSkillsExecutor(),
|
|
21
32
|
},
|
|
22
33
|
enableSkills: true,
|
|
23
34
|
});
|
|
24
35
|
expect(toolsWithExecutor.map((tool) => tool.name)).toContain("skills");
|
|
25
36
|
});
|
|
26
37
|
|
|
38
|
+
it("includes configured skill names in description", () => {
|
|
39
|
+
const executor = createMockSkillsExecutor(
|
|
40
|
+
async () => "ok",
|
|
41
|
+
[
|
|
42
|
+
{ id: "commit", name: "commit", disabled: false },
|
|
43
|
+
{
|
|
44
|
+
id: "review-pr",
|
|
45
|
+
name: "review-pr",
|
|
46
|
+
description: "Review a PR",
|
|
47
|
+
disabled: false,
|
|
48
|
+
},
|
|
49
|
+
{ id: "disabled-skill", name: "disabled-skill", disabled: true },
|
|
50
|
+
],
|
|
51
|
+
);
|
|
52
|
+
const tool = createSkillsTool(executor);
|
|
53
|
+
expect(tool.description).toContain("Available skills: commit, review-pr.");
|
|
54
|
+
expect(tool.description).not.toContain("disabled-skill");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("omits skill list from description when no skills are configured", () => {
|
|
58
|
+
const executor = createMockSkillsExecutor(async () => "ok");
|
|
59
|
+
const tool = createSkillsTool(executor);
|
|
60
|
+
expect(tool.description).not.toContain("Available skills");
|
|
61
|
+
});
|
|
62
|
+
|
|
27
63
|
it("validates and executes skill invocation input", async () => {
|
|
28
64
|
const execute = vi.fn(async () => "loaded");
|
|
29
65
|
const tools = createDefaultTools({
|
|
30
66
|
executors: {
|
|
31
|
-
skills: execute,
|
|
67
|
+
skills: createMockSkillsExecutor(execute),
|
|
32
68
|
},
|
|
33
69
|
enableReadFiles: false,
|
|
34
70
|
enableSearch: false,
|
|
@@ -310,6 +346,48 @@ describe("default read_files tool", () => {
|
|
|
310
346
|
}),
|
|
311
347
|
);
|
|
312
348
|
});
|
|
349
|
+
|
|
350
|
+
it("treats null line bounds as full-file boundaries", async () => {
|
|
351
|
+
const execute = vi.fn(async () => "full file");
|
|
352
|
+
const tool = createReadFilesTool(execute);
|
|
353
|
+
|
|
354
|
+
const result = await tool.execute(
|
|
355
|
+
{
|
|
356
|
+
files: [
|
|
357
|
+
{
|
|
358
|
+
path: "/tmp/example.ts",
|
|
359
|
+
start_line: null,
|
|
360
|
+
end_line: null,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
agentId: "agent-1",
|
|
366
|
+
conversationId: "conv-1",
|
|
367
|
+
iteration: 1,
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
expect(result).toEqual([
|
|
372
|
+
{
|
|
373
|
+
query: "/tmp/example.ts",
|
|
374
|
+
result: "full file",
|
|
375
|
+
success: true,
|
|
376
|
+
},
|
|
377
|
+
]);
|
|
378
|
+
expect(execute).toHaveBeenCalledWith(
|
|
379
|
+
{
|
|
380
|
+
path: "/tmp/example.ts",
|
|
381
|
+
start_line: null,
|
|
382
|
+
end_line: null,
|
|
383
|
+
},
|
|
384
|
+
expect.objectContaining({
|
|
385
|
+
agentId: "agent-1",
|
|
386
|
+
conversationId: "conv-1",
|
|
387
|
+
iteration: 1,
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
});
|
|
313
391
|
});
|
|
314
392
|
|
|
315
393
|
describe("zod schema conversion", () => {
|
|
@@ -329,19 +407,20 @@ describe("zod schema conversion", () => {
|
|
|
329
407
|
"The absolute file path of a text file to read content from",
|
|
330
408
|
},
|
|
331
409
|
start_line: {
|
|
332
|
-
type: "integer",
|
|
333
|
-
description:
|
|
410
|
+
anyOf: [{ type: "integer" }, { type: "null" }],
|
|
411
|
+
description:
|
|
412
|
+
"Optional one-based starting line number to read from; use null or omit for the start of the file",
|
|
334
413
|
},
|
|
335
414
|
end_line: {
|
|
336
|
-
type: "integer",
|
|
415
|
+
anyOf: [{ type: "integer" }, { type: "null" }],
|
|
337
416
|
description:
|
|
338
|
-
"Optional one-based ending line number to read through",
|
|
417
|
+
"Optional one-based ending line number to read through; use null or omit for the end of the file",
|
|
339
418
|
},
|
|
340
419
|
},
|
|
341
420
|
required: ["path"],
|
|
342
421
|
},
|
|
343
422
|
description:
|
|
344
|
-
"Array of file read requests. Omit start_line
|
|
423
|
+
"Array of file read requests. Omit start_line/end_line or set them to null to return the full file content boundaries; provide integers to return only that inclusive one-based line range. Prefer this tool over running terminal command to get file content for better performance and reliability.",
|
|
345
424
|
});
|
|
346
425
|
expect(inputSchema.required).toEqual(["files"]);
|
|
347
426
|
});
|
|
@@ -349,7 +428,7 @@ describe("zod schema conversion", () => {
|
|
|
349
428
|
it("exposes skills args as optional nullable in tool schemas", () => {
|
|
350
429
|
const tools = createDefaultTools({
|
|
351
430
|
executors: {
|
|
352
|
-
skills:
|
|
431
|
+
skills: createMockSkillsExecutor(),
|
|
353
432
|
},
|
|
354
433
|
enableReadFiles: false,
|
|
355
434
|
enableSearch: false,
|