@clinebot/core 0.0.11 → 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.
Files changed (53) hide show
  1. package/dist/agents/agent-config-loader.d.ts +1 -1
  2. package/dist/agents/agent-config-parser.d.ts +5 -2
  3. package/dist/agents/index.d.ts +1 -1
  4. package/dist/agents/plugin-config-loader.d.ts +4 -0
  5. package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
  6. package/dist/agents/plugin-sandbox.d.ts +4 -0
  7. package/dist/index.node.d.ts +1 -0
  8. package/dist/index.node.js +658 -407
  9. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
  10. package/dist/session/default-session-manager.d.ts +5 -0
  11. package/dist/session/session-config-builder.d.ts +4 -1
  12. package/dist/session/session-manager.d.ts +1 -0
  13. package/dist/session/unified-session-persistence-service.d.ts +6 -0
  14. package/dist/session/utils/types.d.ts +9 -0
  15. package/dist/tools/definitions.d.ts +2 -2
  16. package/dist/tools/presets.d.ts +3 -3
  17. package/dist/tools/schemas.d.ts +14 -14
  18. package/dist/types/config.d.ts +5 -0
  19. package/dist/types/events.d.ts +22 -0
  20. package/package.json +5 -4
  21. package/src/agents/agent-config-loader.test.ts +2 -0
  22. package/src/agents/agent-config-loader.ts +1 -0
  23. package/src/agents/agent-config-parser.ts +12 -5
  24. package/src/agents/index.ts +1 -0
  25. package/src/agents/plugin-config-loader.test.ts +49 -0
  26. package/src/agents/plugin-config-loader.ts +10 -73
  27. package/src/agents/plugin-loader.test.ts +128 -2
  28. package/src/agents/plugin-loader.ts +70 -5
  29. package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
  30. package/src/agents/plugin-sandbox.test.ts +198 -1
  31. package/src/agents/plugin-sandbox.ts +223 -353
  32. package/src/index.node.ts +4 -0
  33. package/src/runtime/hook-file-hooks.test.ts +1 -1
  34. package/src/runtime/hook-file-hooks.ts +16 -6
  35. package/src/runtime/runtime-builder.test.ts +67 -0
  36. package/src/runtime/runtime-builder.ts +70 -16
  37. package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
  38. package/src/session/default-session-manager.e2e.test.ts +20 -1
  39. package/src/session/default-session-manager.test.ts +453 -1
  40. package/src/session/default-session-manager.ts +200 -0
  41. package/src/session/session-config-builder.ts +2 -0
  42. package/src/session/session-manager.ts +1 -0
  43. package/src/session/session-team-coordination.ts +30 -0
  44. package/src/session/unified-session-persistence-service.ts +45 -0
  45. package/src/session/utils/types.ts +10 -0
  46. package/src/storage/sqlite-team-store.ts +16 -5
  47. package/src/tools/definitions.test.ts +87 -8
  48. package/src/tools/definitions.ts +89 -70
  49. package/src/tools/presets.test.ts +2 -3
  50. package/src/tools/presets.ts +3 -3
  51. package/src/tools/schemas.ts +23 -22
  52. package/src/types/config.ts +5 -0
  53. 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,
@@ -307,6 +308,8 @@ export class DefaultSessionManager implements SessionManager {
307
308
  activeTeamRunIds: new Set<string>(),
308
309
  pendingTeamRunUpdates: [],
309
310
  teamRunWaiters: [],
311
+ pendingPrompts: [],
312
+ drainingPendingPrompts: false,
310
313
  pluginSandboxShutdown,
311
314
  };
312
315
  this.sessions.set(sessionId, active);
@@ -349,8 +352,18 @@ export class DefaultSessionManager implements SessionManager {
349
352
  promptLength: input.prompt.length,
350
353
  userImageCount: input.userImages?.length ?? 0,
351
354
  userFileCount: input.userFiles?.length ?? 0,
355
+ delivery: input.delivery ?? "immediate",
352
356
  },
353
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
+ }
354
367
  try {
355
368
  const result = await this.runTurn(session, {
356
369
  prompt: input.prompt,
@@ -360,6 +373,9 @@ export class DefaultSessionManager implements SessionManager {
360
373
  if (!session.interactive) {
361
374
  await this.finalizeSingleRun(session, result.finishReason);
362
375
  }
376
+ queueMicrotask(() => {
377
+ void this.drainPendingPrompts(input.sessionId);
378
+ });
363
379
  return result;
364
380
  } catch (error) {
365
381
  await this.failSession(session);
@@ -766,6 +782,151 @@ export class DefaultSessionManager implements SessionManager {
766
782
  this.emitStatus(session.sessionId, status);
767
783
  }
768
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
+
769
930
  // ── Agent event handling ────────────────────────────────────────────
770
931
 
771
932
  private onAgentEvent(
@@ -791,6 +952,45 @@ export class DefaultSessionManager implements SessionManager {
791
952
  handleAgentEvent(ctx, event);
792
953
  }
793
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
+
794
994
  // ── Spawn / sub-agents ──────────────────────────────────────────────
795
995
 
796
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,
@@ -38,6 +38,7 @@ export interface SendSessionInput {
38
38
  prompt: string;
39
39
  userImages?: string[];
40
40
  userFiles?: string[];
41
+ delivery?: "queue" | "steer";
41
42
  }
42
43
 
43
44
  export interface SessionAccumulatedUsage {
@@ -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 ────────────────────────────────────────────
@@ -22,10 +22,20 @@ export type ActiveSession = {
22
22
  activeTeamRunIds: Set<string>;
23
23
  pendingTeamRunUpdates: TeamRunUpdate[];
24
24
  teamRunWaiters: Array<() => void>;
25
+ pendingPrompts: PendingPrompt[];
26
+ drainingPendingPrompts: boolean;
25
27
  pluginSandboxShutdown?: () => Promise<void>;
26
28
  turnUsageBaseline?: SessionAccumulatedUsage;
27
29
  };
28
30
 
31
+ export type PendingPrompt = {
32
+ id: string;
33
+ prompt: string;
34
+ delivery: "queue" | "steer";
35
+ userImages?: string[];
36
+ userFiles?: string[];
37
+ };
38
+
29
39
  export type TeamRunUpdate = {
30
40
  runId: string;
31
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: async () => "ok",
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: "Optional one-based starting line number to read from",
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 and end_line to return the full file content; provide them 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.",
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: async () => "ok",
431
+ skills: createMockSkillsExecutor(),
353
432
  },
354
433
  enableReadFiles: false,
355
434
  enableSearch: false,