@perstack/runtime 0.0.67 → 0.0.69

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.
@@ -11,10 +11,10 @@ import { ProxyAgent, fetch } from 'undici';
11
11
  import { setup, assign, createActor } from 'xstate';
12
12
  import { generateText, tool, jsonSchema } from 'ai';
13
13
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
14
- import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
15
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
16
14
  import { McpError } from '@modelcontextprotocol/sdk/types.js';
17
15
  import { createId } from '@paralleldrive/cuid2';
16
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
17
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
18
18
  import { readFile } from 'fs/promises';
19
19
  import { dedent } from 'ts-dedent';
20
20
  import { ApiV1Client } from '@perstack/api-client/v1';
@@ -22,7 +22,7 @@ import { ApiV1Client } from '@perstack/api-client/v1';
22
22
  // package.json
23
23
  var package_default = {
24
24
  name: "@perstack/runtime",
25
- version: "0.0.67",
25
+ version: "0.0.69",
26
26
  description: "Perstack Runtime",
27
27
  author: "Wintermute Technologies, Inc.",
28
28
  license: "Apache-2.0",
@@ -54,9 +54,6 @@ var package_default = {
54
54
  typecheck: "tsc --noEmit"
55
55
  },
56
56
  dependencies: {
57
- commander: "^14.0.2",
58
- dotenv: "^17.2.3",
59
- "smol-toml": "^1.5.2",
60
57
  "@ai-sdk/amazon-bedrock": "^3.0.62",
61
58
  "@ai-sdk/anthropic": "^2.0.50",
62
59
  "@ai-sdk/azure": "^2.0.77",
@@ -69,14 +66,18 @@ var package_default = {
69
66
  "@perstack/api-client": "workspace:*",
70
67
  "@perstack/core": "workspace:*",
71
68
  ai: "^5.0.104",
69
+ commander: "^14.0.2",
70
+ dotenv: "^17.2.3",
72
71
  "ollama-ai-provider-v2": "^1.5.5",
72
+ "smol-toml": "^1.5.2",
73
73
  "ts-dedent": "^2.2.0",
74
- xstate: "^5.24.0",
75
- undici: "^7.9.0"
74
+ undici: "^7.9.0",
75
+ xstate: "^5.24.0"
76
76
  },
77
77
  devDependencies: {
78
78
  "@tsconfig/node22": "^22.0.5",
79
79
  "@types/node": "^24.10.1",
80
+ memfs: "^4.51.1",
80
81
  tsup: "^8.5.1",
81
82
  typescript: "^5.9.3",
82
83
  vitest: "^4.0.14"
@@ -260,11 +261,11 @@ var BaseSkillManager = class {
260
261
  this._initializing = void 0;
261
262
  }
262
263
  async getToolDefinitions() {
263
- if (!this.isInitialized() && !this.lazyInit) {
264
- throw new Error(`Skill ${this.name} is not initialized`);
264
+ if (!this.isInitialized() && this._initializing) {
265
+ await this._initializing;
265
266
  }
266
- if (!this.isInitialized() && this.lazyInit) {
267
- return [];
267
+ if (!this.isInitialized()) {
268
+ throw new Error(`Skill ${this.name} is not initialized`);
268
269
  }
269
270
  return this._filterTools(this._toolDefinitions);
270
271
  }
@@ -273,6 +274,22 @@ var BaseSkillManager = class {
273
274
  }
274
275
  };
275
276
 
277
+ // src/skill-manager/command-args.ts
278
+ function getCommandArgs(skill) {
279
+ const { name, command, packageName, args } = skill;
280
+ if (!packageName && (!args || args.length === 0)) {
281
+ throw new Error(`Skill ${name} has no packageName or args. Please provide one of them.`);
282
+ }
283
+ if (packageName && args && args.length > 0) {
284
+ throw new Error(`Skill ${name} has both packageName and args. Please provide only one of them.`);
285
+ }
286
+ let newArgs = args && args.length > 0 ? args : [packageName];
287
+ if (command === "npx" && !newArgs.includes("-y")) {
288
+ newArgs = ["-y", ...newArgs];
289
+ }
290
+ return { command, args: newArgs };
291
+ }
292
+
276
293
  // src/skill-manager/delegate.ts
277
294
  var DelegateSkillManager = class extends BaseSkillManager {
278
295
  name;
@@ -334,6 +351,114 @@ var InteractiveSkillManager = class extends BaseSkillManager {
334
351
  return [];
335
352
  }
336
353
  };
354
+
355
+ // src/skill-manager/ip-validator.ts
356
+ function isPrivateOrLocalIP(hostname) {
357
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0") {
358
+ return true;
359
+ }
360
+ const ipv4Match = hostname.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
361
+ if (ipv4Match) {
362
+ const [, a, b] = ipv4Match.map(Number);
363
+ if (a === 10) return true;
364
+ if (a === 172 && b >= 16 && b <= 31) return true;
365
+ if (a === 192 && b === 168) return true;
366
+ if (a === 169 && b === 254) return true;
367
+ if (a === 127) return true;
368
+ }
369
+ if (hostname.includes(":")) {
370
+ if (hostname.startsWith("fe80:")) return true;
371
+ if (hostname.startsWith("fc") || hostname.startsWith("fd")) return true;
372
+ }
373
+ if (hostname.startsWith("::ffff:")) {
374
+ const ipv4Part = hostname.slice(7);
375
+ if (isPrivateOrLocalIP(ipv4Part)) {
376
+ return true;
377
+ }
378
+ }
379
+ return false;
380
+ }
381
+ function handleToolError(error, toolName, McpErrorClass) {
382
+ if (error instanceof McpErrorClass) {
383
+ return [
384
+ {
385
+ type: "textPart",
386
+ text: `Error calling tool ${toolName}: ${error.message}`,
387
+ id: createId()
388
+ }
389
+ ];
390
+ }
391
+ throw error;
392
+ }
393
+ function convertToolResult(result, toolName, input) {
394
+ if (!result.content || result.content.length === 0) {
395
+ return [
396
+ {
397
+ type: "textPart",
398
+ text: `Tool ${toolName} returned nothing with arguments: ${JSON.stringify(input)}`,
399
+ id: createId()
400
+ }
401
+ ];
402
+ }
403
+ return result.content.filter((part) => part.type !== "audio" && part.type !== "resource_link").map((part) => convertPart(part));
404
+ }
405
+ function convertPart(part) {
406
+ switch (part.type) {
407
+ case "text":
408
+ if (!part.text || part.text === "") {
409
+ return { type: "textPart", text: "Error: No content", id: createId() };
410
+ }
411
+ return { type: "textPart", text: part.text, id: createId() };
412
+ case "image":
413
+ if (!part.data || !part.mimeType) {
414
+ throw new Error("Image part must have both data and mimeType");
415
+ }
416
+ return {
417
+ type: "imageInlinePart",
418
+ encodedData: part.data,
419
+ mimeType: part.mimeType,
420
+ id: createId()
421
+ };
422
+ case "resource":
423
+ if (!part.resource) {
424
+ throw new Error("Resource part must have resource content");
425
+ }
426
+ return convertResource(part.resource);
427
+ }
428
+ }
429
+ function convertResource(resource) {
430
+ if (!resource.mimeType) {
431
+ throw new Error(`Resource ${JSON.stringify(resource)} has no mimeType`);
432
+ }
433
+ if (resource.text && typeof resource.text === "string") {
434
+ return { type: "textPart", text: resource.text, id: createId() };
435
+ }
436
+ if (resource.blob && typeof resource.blob === "string") {
437
+ return {
438
+ type: "fileInlinePart",
439
+ encodedData: resource.blob,
440
+ mimeType: resource.mimeType,
441
+ id: createId()
442
+ };
443
+ }
444
+ throw new Error(`Unsupported resource type: ${JSON.stringify(resource)}`);
445
+ }
446
+ var DefaultTransportFactory = class {
447
+ createStdio(options) {
448
+ return new StdioClientTransport({
449
+ command: options.command,
450
+ args: options.args,
451
+ env: options.env,
452
+ stderr: options.stderr
453
+ });
454
+ }
455
+ createSse(options) {
456
+ return new SSEClientTransport(options.url);
457
+ }
458
+ };
459
+ var defaultTransportFactory = new DefaultTransportFactory();
460
+
461
+ // src/skill-manager/mcp.ts
337
462
  var McpSkillManager = class extends BaseSkillManager {
338
463
  name;
339
464
  type = "mcp";
@@ -341,11 +466,13 @@ var McpSkillManager = class extends BaseSkillManager {
341
466
  skill;
342
467
  _mcpClient;
343
468
  _env;
344
- constructor(skill, env, jobId, runId, eventListener) {
469
+ _transportFactory;
470
+ constructor(skill, env, jobId, runId, eventListener, options) {
345
471
  super(jobId, runId, eventListener);
346
472
  this.name = skill.name;
347
473
  this.skill = skill;
348
474
  this._env = env;
475
+ this._transportFactory = options?.transportFactory ?? defaultTransportFactory;
349
476
  this.lazyInit = skill.type === "mcpStdioSkill" && skill.lazyInit && skill.name !== "@perstack/base";
350
477
  }
351
478
  async _doInit() {
@@ -353,12 +480,15 @@ var McpSkillManager = class extends BaseSkillManager {
353
480
  name: `${this.skill.name}-mcp-client`,
354
481
  version: "1.0.0"
355
482
  });
483
+ let timingInfo;
356
484
  if (this.skill.type === "mcpStdioSkill") {
357
- await this._initStdio(this.skill);
485
+ timingInfo = await this._initStdio(this.skill);
358
486
  } else {
359
487
  await this._initSse(this.skill);
360
488
  }
489
+ const toolDiscoveryStartTime = Date.now();
361
490
  const { tools } = await this._mcpClient.listTools();
491
+ const toolDiscoveryDurationMs = Date.now() - toolDiscoveryStartTime;
362
492
  this._toolDefinitions = tools.map((tool2) => ({
363
493
  skillName: this.skill.name,
364
494
  name: tool2.name,
@@ -366,6 +496,19 @@ var McpSkillManager = class extends BaseSkillManager {
366
496
  inputSchema: tool2.inputSchema,
367
497
  interactive: false
368
498
  }));
499
+ if (this._eventListener && timingInfo) {
500
+ const totalDurationMs = Date.now() - timingInfo.startTime;
501
+ const event = createRuntimeEvent("skillConnected", this._jobId, this._runId, {
502
+ skillName: this.skill.name,
503
+ serverInfo: timingInfo.serverInfo,
504
+ spawnDurationMs: timingInfo.spawnDurationMs,
505
+ handshakeDurationMs: timingInfo.handshakeDurationMs,
506
+ toolDiscoveryDurationMs,
507
+ connectDurationMs: timingInfo.spawnDurationMs + timingInfo.handshakeDurationMs,
508
+ totalDurationMs
509
+ });
510
+ this._eventListener(event);
511
+ }
369
512
  }
370
513
  async _initStdio(skill) {
371
514
  if (!skill.command) {
@@ -380,7 +523,7 @@ var McpSkillManager = class extends BaseSkillManager {
380
523
  }
381
524
  const env = getFilteredEnv(requiredEnv);
382
525
  const startTime = Date.now();
383
- const { command, args } = this._getCommandArgs(skill);
526
+ const { command, args } = getCommandArgs(skill);
384
527
  if (this._eventListener) {
385
528
  const event = createRuntimeEvent("skillStarting", this._jobId, this._runId, {
386
529
  skillName: skill.name,
@@ -389,7 +532,8 @@ var McpSkillManager = class extends BaseSkillManager {
389
532
  });
390
533
  this._eventListener(event);
391
534
  }
392
- const transport = new StdioClientTransport({ command, args, env, stderr: "pipe" });
535
+ const transport = this._transportFactory.createStdio({ command, args, env, stderr: "pipe" });
536
+ const spawnDurationMs = Date.now() - startTime;
393
537
  if (transport.stderr) {
394
538
  transport.stderr.on("data", (chunk) => {
395
539
  if (this._eventListener) {
@@ -403,17 +547,14 @@ var McpSkillManager = class extends BaseSkillManager {
403
547
  }
404
548
  const connectStartTime = Date.now();
405
549
  await this._mcpClient.connect(transport);
406
- const connectTime = Date.now();
407
- if (this._eventListener) {
408
- const serverInfo = this._mcpClient.getServerVersion();
409
- const event = createRuntimeEvent("skillConnected", this._jobId, this._runId, {
410
- skillName: skill.name,
411
- serverInfo: serverInfo ? { name: serverInfo.name, version: serverInfo.version } : void 0,
412
- connectDurationMs: connectTime - connectStartTime,
413
- totalDurationMs: connectTime - startTime
414
- });
415
- this._eventListener(event);
416
- }
550
+ const handshakeDurationMs = Date.now() - connectStartTime;
551
+ const serverVersion = this._mcpClient.getServerVersion();
552
+ return {
553
+ startTime,
554
+ spawnDurationMs,
555
+ handshakeDurationMs,
556
+ serverInfo: serverVersion ? { name: serverVersion.name, version: serverVersion.version } : void 0
557
+ };
417
558
  }
418
559
  async _initSse(skill) {
419
560
  if (!skill.endpoint) {
@@ -423,56 +564,14 @@ var McpSkillManager = class extends BaseSkillManager {
423
564
  if (url.protocol !== "https:") {
424
565
  throw new Error(`Skill ${skill.name} SSE endpoint must use HTTPS: ${skill.endpoint}`);
425
566
  }
426
- if (this._isPrivateOrLocalIP(url.hostname)) {
567
+ if (isPrivateOrLocalIP(url.hostname)) {
427
568
  throw new Error(
428
569
  `Skill ${skill.name} SSE endpoint cannot use private/local IP: ${skill.endpoint}`
429
570
  );
430
571
  }
431
- const transport = new SSEClientTransport(url);
572
+ const transport = this._transportFactory.createSse({ url });
432
573
  await this._mcpClient.connect(transport);
433
574
  }
434
- _isPrivateOrLocalIP(hostname) {
435
- if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0") {
436
- return true;
437
- }
438
- const ipv4Match = hostname.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
439
- if (ipv4Match) {
440
- const [, a, b] = ipv4Match.map(Number);
441
- if (a === 10) return true;
442
- if (a === 172 && b >= 16 && b <= 31) return true;
443
- if (a === 192 && b === 168) return true;
444
- if (a === 169 && b === 254) return true;
445
- if (a === 127) return true;
446
- }
447
- if (hostname.includes(":")) {
448
- if (hostname.startsWith("fe80:") || hostname.startsWith("fc") || hostname.startsWith("fd")) {
449
- return true;
450
- }
451
- }
452
- if (hostname.startsWith("::ffff:")) {
453
- const ipv4Part = hostname.slice(7);
454
- if (this._isPrivateOrLocalIP(ipv4Part)) {
455
- return true;
456
- }
457
- }
458
- return false;
459
- }
460
- _getCommandArgs(skill) {
461
- const { name, command, packageName, args } = skill;
462
- if (!packageName && (!args || args.length === 0)) {
463
- throw new Error(`Skill ${name} has no packageName or args. Please provide one of them.`);
464
- }
465
- if (packageName && args && args.length > 0) {
466
- throw new Error(
467
- `Skill ${name} has both packageName and args. Please provide only one of them.`
468
- );
469
- }
470
- let newArgs = args && args.length > 0 ? args : [packageName];
471
- if (command === "npx" && !newArgs.includes("-y")) {
472
- newArgs = ["-y", ...newArgs];
473
- }
474
- return { command, args: newArgs };
475
- }
476
575
  async close() {
477
576
  if (this._mcpClient) {
478
577
  await this._mcpClient.close();
@@ -498,77 +597,33 @@ var McpSkillManager = class extends BaseSkillManager {
498
597
  name: toolName,
499
598
  arguments: input
500
599
  });
501
- return this._convertToolResult(result, toolName, input);
600
+ return convertToolResult(result, toolName, input);
502
601
  } catch (error) {
503
- return this._handleToolError(error, toolName);
602
+ return handleToolError(error, toolName, McpError);
504
603
  }
505
604
  }
506
- _handleToolError(error, toolName) {
507
- if (error instanceof McpError) {
508
- return [
509
- {
510
- type: "textPart",
511
- text: `Error calling tool ${toolName}: ${error.message}`,
512
- id: createId()
513
- }
514
- ];
515
- }
516
- throw error;
605
+ };
606
+
607
+ // src/skill-manager/skill-manager-factory.ts
608
+ var DefaultSkillManagerFactory = class {
609
+ createMcp(skill, context) {
610
+ return new McpSkillManager(
611
+ skill,
612
+ context.env,
613
+ context.jobId,
614
+ context.runId,
615
+ context.eventListener,
616
+ context.mcpOptions
617
+ );
517
618
  }
518
- _convertToolResult(result, toolName, input) {
519
- if (!result.content || result.content.length === 0) {
520
- return [
521
- {
522
- type: "textPart",
523
- text: `Tool ${toolName} returned nothing with arguments: ${JSON.stringify(input)}`,
524
- id: createId()
525
- }
526
- ];
527
- }
528
- return result.content.filter((part) => part.type !== "audio" && part.type !== "resource_link").map((part) => this._convertPart(part));
619
+ createInteractive(skill, context) {
620
+ return new InteractiveSkillManager(skill, context.jobId, context.runId, context.eventListener);
529
621
  }
530
- _convertPart(part) {
531
- switch (part.type) {
532
- case "text":
533
- if (!part.text || part.text === "") {
534
- return { type: "textPart", text: "Error: No content", id: createId() };
535
- }
536
- return { type: "textPart", text: part.text, id: createId() };
537
- case "image":
538
- if (!part.data || !part.mimeType) {
539
- throw new Error("Image part must have both data and mimeType");
540
- }
541
- return {
542
- type: "imageInlinePart",
543
- encodedData: part.data,
544
- mimeType: part.mimeType,
545
- id: createId()
546
- };
547
- case "resource":
548
- if (!part.resource) {
549
- throw new Error("Resource part must have resource content");
550
- }
551
- return this._convertResource(part.resource);
552
- }
553
- }
554
- _convertResource(resource) {
555
- if (!resource.mimeType) {
556
- throw new Error(`Resource ${JSON.stringify(resource)} has no mimeType`);
557
- }
558
- if (resource.text && typeof resource.text === "string") {
559
- return { type: "textPart", text: resource.text, id: createId() };
560
- }
561
- if (resource.blob && typeof resource.blob === "string") {
562
- return {
563
- type: "fileInlinePart",
564
- encodedData: resource.blob,
565
- mimeType: resource.mimeType,
566
- id: createId()
567
- };
568
- }
569
- throw new Error(`Unsupported resource type: ${JSON.stringify(resource)}`);
622
+ createDelegate(expert, context) {
623
+ return new DelegateSkillManager(expert, context.jobId, context.runId, context.eventListener);
570
624
  }
571
625
  };
626
+ var defaultSkillManagerFactory = new DefaultSkillManagerFactory();
572
627
 
573
628
  // src/skill-manager/helpers.ts
574
629
  async function initSkillManagersWithCleanup(managers, allManagers) {
@@ -583,11 +638,20 @@ async function initSkillManagersWithCleanup(managers, allManagers) {
583
638
  async function getSkillManagers(expert, experts, setting, eventListener, options) {
584
639
  const { perstackBaseSkillCommand, env, jobId, runId } = setting;
585
640
  const { skills } = expert;
641
+ const factory = options?.factory ?? defaultSkillManagerFactory;
586
642
  if (!skills["@perstack/base"]) {
587
643
  throw new Error("Base skill is not defined");
588
644
  }
645
+ const factoryContext = {
646
+ env,
647
+ jobId,
648
+ runId,
649
+ eventListener
650
+ };
589
651
  const allManagers = [];
590
- const mcpSkills = Object.values(skills).filter((skill) => skill.type === "mcpStdioSkill" || skill.type === "mcpSseSkill").map((skill) => {
652
+ const mcpSkills = Object.values(skills).filter(
653
+ (skill) => skill.type === "mcpStdioSkill" || skill.type === "mcpSseSkill"
654
+ ).map((skill) => {
591
655
  if (perstackBaseSkillCommand && skill.type === "mcpStdioSkill") {
592
656
  const matchesBaseByPackage = skill.command === "npx" && skill.packageName === "@perstack/base";
593
657
  const matchesBaseByArgs = skill.command === "npx" && Array.isArray(skill.args) && skill.args.includes("@perstack/base");
@@ -608,7 +672,7 @@ async function getSkillManagers(expert, experts, setting, eventListener, options
608
672
  return skill;
609
673
  });
610
674
  const mcpSkillManagers = mcpSkills.map((skill) => {
611
- const manager = new McpSkillManager(skill, env, jobId, runId, eventListener);
675
+ const manager = factory.createMcp(skill, factoryContext);
612
676
  allManagers.push(manager);
613
677
  return manager;
614
678
  });
@@ -618,7 +682,7 @@ async function getSkillManagers(expert, experts, setting, eventListener, options
618
682
  (skill) => skill.type === "interactiveSkill"
619
683
  );
620
684
  const interactiveSkillManagers = interactiveSkills.map((interactiveSkill) => {
621
- const manager = new InteractiveSkillManager(interactiveSkill, jobId, runId, eventListener);
685
+ const manager = factory.createInteractive(interactiveSkill, factoryContext);
622
686
  allManagers.push(manager);
623
687
  return manager;
624
688
  });
@@ -632,7 +696,7 @@ async function getSkillManagers(expert, experts, setting, eventListener, options
632
696
  })));
633
697
  throw new Error(`Delegate expert "${delegateExpertName}" not found in experts`);
634
698
  }
635
- const manager = new DelegateSkillManager(delegate, jobId, runId, eventListener);
699
+ const manager = factory.createDelegate(delegate, factoryContext);
636
700
  allManagers.push(manager);
637
701
  delegateSkillManagers.push(manager);
638
702
  }
@@ -671,12 +735,140 @@ async function getToolSet(skillManagers) {
671
735
  }
672
736
  return tools;
673
737
  }
738
+ function isFileInfo(value) {
739
+ return typeof value === "object" && value !== null && "path" in value && "mimeType" in value && "size" in value && typeof value.path === "string" && typeof value.mimeType === "string" && typeof value.size === "number";
740
+ }
741
+ async function processFileToolResult(toolResult, toolName) {
742
+ const processedContents = [];
743
+ for (const part of toolResult.result) {
744
+ if (part.type !== "textPart") {
745
+ processedContents.push(part);
746
+ continue;
747
+ }
748
+ let fileInfo;
749
+ try {
750
+ const parsed = JSON.parse(part.text);
751
+ if (isFileInfo(parsed)) {
752
+ fileInfo = parsed;
753
+ }
754
+ } catch {
755
+ processedContents.push(part);
756
+ continue;
757
+ }
758
+ if (!fileInfo) {
759
+ processedContents.push(part);
760
+ continue;
761
+ }
762
+ const { path, mimeType } = fileInfo;
763
+ try {
764
+ const buffer = await readFile(path);
765
+ if (toolName === "readImageFile") {
766
+ processedContents.push({
767
+ type: "imageInlinePart",
768
+ id: part.id,
769
+ encodedData: buffer.toString("base64"),
770
+ mimeType
771
+ });
772
+ } else {
773
+ processedContents.push({
774
+ type: "fileInlinePart",
775
+ id: part.id,
776
+ encodedData: buffer.toString("base64"),
777
+ mimeType
778
+ });
779
+ }
780
+ } catch (error) {
781
+ processedContents.push({
782
+ type: "textPart",
783
+ id: part.id,
784
+ text: `Failed to read file "${path}": ${error instanceof Error ? error.message : String(error)}`
785
+ });
786
+ }
787
+ }
788
+ return { ...toolResult, result: processedContents };
789
+ }
790
+ var McpToolExecutor = class {
791
+ type = "mcp";
792
+ async execute(toolCall, skillManagers) {
793
+ const skillManager = await getSkillManagerByToolName(skillManagers, toolCall.toolName);
794
+ if (skillManager.type !== "mcp") {
795
+ throw new Error(`Incorrect SkillType, required MCP, got ${skillManager.type}`);
796
+ }
797
+ const result = await skillManager.callTool(
798
+ toolCall.toolName,
799
+ toolCall.args
800
+ );
801
+ const toolResult = {
802
+ id: toolCall.id,
803
+ skillName: toolCall.skillName,
804
+ toolName: toolCall.toolName,
805
+ result
806
+ };
807
+ if (toolCall.toolName === "readPdfFile" || toolCall.toolName === "readImageFile") {
808
+ return processFileToolResult(toolResult, toolCall.toolName);
809
+ }
810
+ return toolResult;
811
+ }
812
+ };
674
813
 
675
- // src/state-machine/states/calling-delegate.ts
676
- async function getToolType(toolName, skillManagers) {
814
+ // src/tool-execution/executor-factory.ts
815
+ var ToolExecutorFactory = class {
816
+ executors;
817
+ constructor() {
818
+ this.executors = /* @__PURE__ */ new Map([
819
+ ["mcp", new McpToolExecutor()]
820
+ // delegate and interactive are handled specially (not executed here)
821
+ ]);
822
+ }
823
+ /**
824
+ * Get the executor for a given skill type
825
+ */
826
+ getExecutor(type) {
827
+ return this.executors.get(type);
828
+ }
829
+ /**
830
+ * Execute a tool call using the appropriate executor
831
+ */
832
+ async execute(toolCall, type, skillManagers) {
833
+ const executor = this.executors.get(type);
834
+ if (!executor) {
835
+ throw new Error(`No executor registered for skill type: ${type}`);
836
+ }
837
+ return executor.execute(toolCall, skillManagers);
838
+ }
839
+ /**
840
+ * Check if a skill type can be executed locally (vs requiring delegation)
841
+ */
842
+ canExecuteLocally(type) {
843
+ return type === "mcp";
844
+ }
845
+ };
846
+ var toolExecutorFactory = new ToolExecutorFactory();
847
+
848
+ // src/tool-execution/tool-classifier.ts
849
+ async function getToolTypeByName(toolName, skillManagers) {
677
850
  const skillManager = await getSkillManagerByToolName(skillManagers, toolName);
678
851
  return skillManager.type;
679
852
  }
853
+ async function classifyToolCalls(toolCalls, skillManagers) {
854
+ const classified = {
855
+ mcp: [],
856
+ delegate: [],
857
+ interactive: []
858
+ };
859
+ const results = await Promise.all(
860
+ toolCalls.map(async (toolCall) => {
861
+ const skillManager = await getSkillManagerByToolName(skillManagers, toolCall.toolName);
862
+ return { toolCall, type: skillManager.type, skillManager };
863
+ })
864
+ );
865
+ for (const result of results) {
866
+ classified[result.type].push(result);
867
+ }
868
+ return classified;
869
+ }
870
+
871
+ // src/state-machine/states/calling-delegate.ts
680
872
  async function callingDelegateLogic({
681
873
  setting,
682
874
  checkpoint,
@@ -689,7 +881,7 @@ async function callingDelegateLogic({
689
881
  const toolCallTypes = await Promise.all(
690
882
  step.pendingToolCalls.map(async (tc) => ({
691
883
  toolCall: tc,
692
- type: await getToolType(tc.toolName, skillManagers)
884
+ type: await getToolTypeByName(tc.toolName, skillManagers)
693
885
  }))
694
886
  );
695
887
  const delegateToolCalls = toolCallTypes.filter((t) => t.type === "delegate").map((t) => t.toolCall);
@@ -767,79 +959,6 @@ function hasRemainingTodos(toolResult) {
767
959
  return false;
768
960
  }
769
961
  }
770
- function isFileInfo(value) {
771
- return typeof value === "object" && value !== null && "path" in value && "mimeType" in value && "size" in value && typeof value.path === "string" && typeof value.mimeType === "string" && typeof value.size === "number";
772
- }
773
- async function processFileToolResult(toolResult, toolName) {
774
- const processedContents = [];
775
- for (const part of toolResult.result) {
776
- if (part.type !== "textPart") {
777
- processedContents.push(part);
778
- continue;
779
- }
780
- let fileInfo;
781
- try {
782
- const parsed = JSON.parse(part.text);
783
- if (isFileInfo(parsed)) {
784
- fileInfo = parsed;
785
- }
786
- } catch {
787
- processedContents.push(part);
788
- continue;
789
- }
790
- if (!fileInfo) {
791
- processedContents.push(part);
792
- continue;
793
- }
794
- const { path, mimeType } = fileInfo;
795
- try {
796
- const buffer = await readFile(path);
797
- if (toolName === "readImageFile") {
798
- processedContents.push({
799
- type: "imageInlinePart",
800
- id: part.id,
801
- encodedData: buffer.toString("base64"),
802
- mimeType
803
- });
804
- } else {
805
- processedContents.push({
806
- type: "fileInlinePart",
807
- id: part.id,
808
- encodedData: buffer.toString("base64"),
809
- mimeType
810
- });
811
- }
812
- } catch (error) {
813
- processedContents.push({
814
- type: "textPart",
815
- id: part.id,
816
- text: `Failed to read file "${path}": ${error instanceof Error ? error.message : String(error)}`
817
- });
818
- }
819
- }
820
- return { ...toolResult, result: processedContents };
821
- }
822
- async function executeMcpToolCall(toolCall, skillManagers) {
823
- const skillManager = await getSkillManagerByToolName(skillManagers, toolCall.toolName);
824
- if (skillManager.type !== "mcp") {
825
- throw new Error(`Incorrect SkillType, required MCP, got ${skillManager.type}`);
826
- }
827
- const result = await skillManager.callTool(toolCall.toolName, toolCall.args);
828
- const toolResult = {
829
- id: toolCall.id,
830
- skillName: toolCall.skillName,
831
- toolName: toolCall.toolName,
832
- result
833
- };
834
- if (toolCall.toolName === "readPdfFile" || toolCall.toolName === "readImageFile") {
835
- return processFileToolResult(toolResult, toolCall.toolName);
836
- }
837
- return toolResult;
838
- }
839
- async function getToolType2(toolCall, skillManagers) {
840
- const skillManager = await getSkillManagerByToolName(skillManagers, toolCall.toolName);
841
- return skillManager.type;
842
- }
843
962
  async function callingToolLogic({
844
963
  setting,
845
964
  checkpoint,
@@ -855,28 +974,26 @@ async function callingToolLogic({
855
974
  (tc) => tc.skillName === "@perstack/base" && tc.toolName === "attemptCompletion"
856
975
  );
857
976
  if (attemptCompletionTool) {
858
- const toolResult = await executeMcpToolCall(attemptCompletionTool, skillManagers);
977
+ const toolResult = await toolExecutorFactory.execute(
978
+ attemptCompletionTool,
979
+ "mcp",
980
+ skillManagers
981
+ );
859
982
  if (hasRemainingTodos(toolResult)) {
860
983
  return resolveToolResults(setting, checkpoint, { toolResults: [toolResult] });
861
984
  }
862
985
  return attemptCompletion(setting, checkpoint, { toolResult });
863
986
  }
864
- const toolCallTypes = await Promise.all(
865
- pendingToolCalls.map(async (tc) => ({
866
- toolCall: tc,
867
- type: await getToolType2(tc, skillManagers)
868
- }))
869
- );
870
- const mcpToolCalls = toolCallTypes.filter((t) => t.type === "mcp").map((t) => t.toolCall);
871
- const delegateToolCalls = toolCallTypes.filter((t) => t.type === "delegate").map((t) => t.toolCall);
872
- const interactiveToolCalls = toolCallTypes.filter((t) => t.type === "interactive").map((t) => t.toolCall);
873
- if (mcpToolCalls.length > 0) {
987
+ const classified = await classifyToolCalls(pendingToolCalls, skillManagers);
988
+ if (classified.mcp.length > 0) {
874
989
  const mcpResults = await Promise.all(
875
- mcpToolCalls.map((tc) => executeMcpToolCall(tc, skillManagers))
990
+ classified.mcp.map((c) => toolExecutorFactory.execute(c.toolCall, "mcp", skillManagers))
876
991
  );
877
992
  toolResults.push(...mcpResults);
878
993
  }
879
- if (delegateToolCalls.length > 0) {
994
+ if (classified.delegate.length > 0) {
995
+ const delegateToolCalls = classified.delegate.map((c) => c.toolCall);
996
+ const interactiveToolCalls = classified.interactive.map((c) => c.toolCall);
880
997
  step.partialToolResults = toolResults;
881
998
  step.pendingToolCalls = [...delegateToolCalls, ...interactiveToolCalls];
882
999
  return callDelegate(setting, checkpoint, {
@@ -885,11 +1002,12 @@ async function callingToolLogic({
885
1002
  usage: step.usage
886
1003
  });
887
1004
  }
888
- if (interactiveToolCalls.length > 0) {
889
- const interactiveToolCall = interactiveToolCalls[0];
1005
+ if (classified.interactive.length > 0) {
1006
+ const interactiveToolCall = classified.interactive[0]?.toolCall;
890
1007
  if (!interactiveToolCall) {
891
1008
  throw new Error("No interactive tool call found");
892
1009
  }
1010
+ const interactiveToolCalls = classified.interactive.map((c) => c.toolCall);
893
1011
  step.partialToolResults = toolResults;
894
1012
  step.pendingToolCalls = interactiveToolCalls;
895
1013
  return callInteractiveTool(setting, checkpoint, {
@@ -1198,7 +1316,7 @@ async function generatingRunResultLogic({
1198
1316
  usage
1199
1317
  });
1200
1318
  }
1201
- async function classifyToolCalls(toolCalls, skillManagers) {
1319
+ async function classifyToolCalls2(toolCalls, skillManagers) {
1202
1320
  return Promise.all(
1203
1321
  toolCalls.map(async (tc) => {
1204
1322
  const skillManager = await getSkillManagerByToolName(skillManagers, tc.toolName);
@@ -1275,7 +1393,7 @@ async function generatingToolCallLogic({
1275
1393
  usage
1276
1394
  });
1277
1395
  }
1278
- const classified = await classifyToolCalls(toolCalls, skillManagers);
1396
+ const classified = await classifyToolCalls2(toolCalls, skillManagers);
1279
1397
  const sorted = sortToolCallsByPriority(classified);
1280
1398
  if (finishReason === "tool-calls" || finishReason === "stop") {
1281
1399
  const toolCallParts = buildToolCallParts(sorted);
@@ -1905,88 +2023,115 @@ var StateMachineLogics = {
1905
2023
  CallingDelegate: callingDelegateLogic,
1906
2024
  FinishingStep: finishingStepLogic
1907
2025
  };
1908
- async function executeStateMachine(params) {
1909
- const {
1910
- setting,
1911
- initialCheckpoint,
1912
- eventListener,
1913
- skillManagers,
1914
- eventEmitter,
1915
- storeCheckpoint,
1916
- shouldContinueRun
1917
- } = params;
1918
- const runActor = createActor(runtimeStateMachine, {
1919
- input: {
1920
- setting,
1921
- initialCheckpoint,
1922
- eventListener,
1923
- skillManagers
1924
- }
1925
- });
1926
- return new Promise((resolve, reject) => {
1927
- runActor.subscribe(async (runState) => {
1928
- try {
1929
- if (runState.value === "Stopped") {
1930
- const { checkpoint, skillManagers: skillManagers2 } = runState.context;
1931
- if (!checkpoint) {
1932
- throw new Error("Checkpoint is undefined");
1933
- }
1934
- await closeSkillManagers(skillManagers2);
1935
- resolve(checkpoint);
1936
- } else {
1937
- const event = await StateMachineLogics[runState.value](runState.context);
1938
- if ("checkpoint" in event) {
1939
- await storeCheckpoint(event.checkpoint);
1940
- }
1941
- await eventEmitter.emit(event);
1942
- if (shouldContinueRun) {
1943
- const shouldContinue = await shouldContinueRun(
1944
- runState.context.setting,
1945
- runState.context.checkpoint,
1946
- runState.context.step
1947
- );
1948
- if (!shouldContinue) {
1949
- runActor.stop();
1950
- await closeSkillManagers(runState.context.skillManagers);
1951
- resolve(runState.context.checkpoint);
1952
- return;
1953
- }
1954
- }
1955
- runActor.send(event);
1956
- }
1957
- } catch (error) {
1958
- await closeSkillManagers(skillManagers).catch(() => {
1959
- });
1960
- reject(error);
2026
+ var DefaultActorFactory = class {
2027
+ create(input) {
2028
+ return createActor(runtimeStateMachine, input);
2029
+ }
2030
+ };
2031
+ var defaultActorFactory = new DefaultActorFactory();
2032
+
2033
+ // src/state-machine/coordinator.ts
2034
+ var StateMachineCoordinator = class {
2035
+ constructor(params, deps = {}) {
2036
+ this.params = params;
2037
+ this.actorFactory = deps.actorFactory ?? defaultActorFactory;
2038
+ this.closeManagers = deps.closeSkillManagers ?? closeSkillManagers;
2039
+ this.logics = deps.logics ?? StateMachineLogics;
2040
+ }
2041
+ actorFactory;
2042
+ closeManagers;
2043
+ logics;
2044
+ actor = null;
2045
+ resolvePromise = null;
2046
+ rejectPromise = null;
2047
+ /**
2048
+ * Execute the state machine and return the final checkpoint.
2049
+ */
2050
+ async execute() {
2051
+ const { setting, initialCheckpoint, eventListener, skillManagers } = this.params;
2052
+ this.actor = this.actorFactory.create({
2053
+ input: {
2054
+ setting,
2055
+ initialCheckpoint,
2056
+ eventListener,
2057
+ skillManagers
1961
2058
  }
1962
2059
  });
1963
- runActor.start();
1964
- });
1965
- }
1966
- var RunEventEmitter = class {
1967
- listeners = [];
1968
- subscribe(listener) {
1969
- this.listeners.push(listener);
1970
- }
1971
- async emit(event) {
1972
- const errors = [];
1973
- for (const listener of this.listeners) {
1974
- try {
1975
- await listener({
1976
- ...event,
1977
- id: createId(),
1978
- timestamp: Date.now()
2060
+ return new Promise((resolve, reject) => {
2061
+ this.resolvePromise = resolve;
2062
+ this.rejectPromise = reject;
2063
+ this.actor.subscribe((runState) => {
2064
+ this.handleStateChange(runState).catch((error) => {
2065
+ this.handleError(error);
1979
2066
  });
1980
- } catch (error) {
1981
- errors.push(error instanceof Error ? error : new Error(String(error)));
2067
+ });
2068
+ this.actor.start();
2069
+ });
2070
+ }
2071
+ /**
2072
+ * Handle state changes from the actor.
2073
+ * Exported for testing purposes.
2074
+ */
2075
+ async handleStateChange(runState) {
2076
+ if (runState.value === "Stopped") {
2077
+ await this.handleStoppedState(runState);
2078
+ } else {
2079
+ await this.handleActiveState(runState);
2080
+ }
2081
+ }
2082
+ /**
2083
+ * Handle the stopped state - cleanup and resolve.
2084
+ */
2085
+ async handleStoppedState(runState) {
2086
+ const { checkpoint, skillManagers } = runState.context;
2087
+ if (!checkpoint) {
2088
+ throw new Error("Checkpoint is undefined");
2089
+ }
2090
+ await this.closeManagers(skillManagers);
2091
+ this.resolvePromise?.(checkpoint);
2092
+ }
2093
+ /**
2094
+ * Handle active states - execute logic, store checkpoint, emit events.
2095
+ */
2096
+ async handleActiveState(runState) {
2097
+ const { eventEmitter, storeCheckpoint, shouldContinueRun } = this.params;
2098
+ const stateValue = runState.value;
2099
+ const event = await this.logics[stateValue](runState.context);
2100
+ if ("checkpoint" in event) {
2101
+ await storeCheckpoint(event.checkpoint);
2102
+ }
2103
+ await eventEmitter.emit(event);
2104
+ if (shouldContinueRun) {
2105
+ const shouldContinue = await shouldContinueRun(
2106
+ runState.context.setting,
2107
+ runState.context.checkpoint,
2108
+ runState.context.step
2109
+ );
2110
+ if (!shouldContinue) {
2111
+ this.actor?.stop();
2112
+ await this.closeManagers(runState.context.skillManagers);
2113
+ this.resolvePromise?.(runState.context.checkpoint);
2114
+ return;
1982
2115
  }
1983
2116
  }
1984
- if (errors.length > 0) {
1985
- throw new AggregateError(errors, "One or more event listeners failed");
1986
- }
2117
+ this.actor?.send(event);
2118
+ }
2119
+ /**
2120
+ * Handle errors - cleanup and reject.
2121
+ */
2122
+ async handleError(error) {
2123
+ await this.closeManagers(this.params.skillManagers).catch(() => {
2124
+ });
2125
+ this.rejectPromise?.(error instanceof Error ? error : new Error(String(error)));
1987
2126
  }
1988
2127
  };
1989
2128
 
2129
+ // src/state-machine/executor.ts
2130
+ async function executeStateMachine(params) {
2131
+ const coordinator = new StateMachineCoordinator(params);
2132
+ return coordinator.execute();
2133
+ }
2134
+
1990
2135
  // src/helpers/checkpoint.ts
1991
2136
  function createInitialCheckpoint(checkpointId, params) {
1992
2137
  return {
@@ -2049,46 +2194,6 @@ function buildDelegationReturnState(currentSetting, resultCheckpoint, parentChec
2049
2194
  }
2050
2195
  };
2051
2196
  }
2052
- function buildDelegateToState(currentSetting, resultCheckpoint, currentExpert) {
2053
- const { delegateTo } = resultCheckpoint;
2054
- if (!delegateTo || delegateTo.length === 0) {
2055
- throw new Error("delegateTo is required for buildDelegateToState");
2056
- }
2057
- const firstDelegation = delegateTo[0];
2058
- const { expert, toolCallId, toolName, query } = firstDelegation;
2059
- return {
2060
- setting: {
2061
- ...currentSetting,
2062
- expertKey: expert.key,
2063
- input: {
2064
- text: query
2065
- }
2066
- },
2067
- checkpoint: {
2068
- ...resultCheckpoint,
2069
- status: "init",
2070
- messages: [],
2071
- expert: {
2072
- key: expert.key,
2073
- name: expert.name,
2074
- version: expert.version
2075
- },
2076
- delegatedBy: {
2077
- expert: {
2078
- key: currentExpert.key,
2079
- name: currentExpert.name,
2080
- version: currentExpert.version
2081
- },
2082
- toolCallId,
2083
- toolName,
2084
- checkpointId: resultCheckpoint.id
2085
- },
2086
- usage: resultCheckpoint.usage,
2087
- pendingToolCalls: void 0,
2088
- partialToolResults: void 0
2089
- }
2090
- };
2091
- }
2092
2197
  async function resolveExpertToRun(expertKey, experts, clientOptions) {
2093
2198
  if (experts[expertKey]) {
2094
2199
  return experts[expertKey];
@@ -2138,68 +2243,245 @@ async function setupExperts(setting, resolveExpertToRun2 = resolveExpertToRun) {
2138
2243
  }
2139
2244
  return { expertToRun, experts };
2140
2245
  }
2141
-
2142
- // src/run.ts
2143
- var noopStoreCheckpoint = async () => {
2144
- };
2145
- var noopStoreEvent = async () => {
2146
- };
2147
- var noopStoreJob = () => {
2246
+ var SingleDelegationStrategy = class {
2247
+ async execute(delegations, setting, context, parentExpert, _runFn, _parentOptions) {
2248
+ if (delegations.length !== 1) {
2249
+ throw new Error("SingleDelegationStrategy requires exactly one delegation");
2250
+ }
2251
+ const delegation = delegations[0];
2252
+ const { expert, toolCallId, toolName, query } = delegation;
2253
+ const nextSetting = {
2254
+ ...setting,
2255
+ expertKey: expert.key,
2256
+ input: { text: query }
2257
+ };
2258
+ const nextCheckpoint = {
2259
+ id: context.id,
2260
+ jobId: setting.jobId,
2261
+ runId: setting.runId,
2262
+ status: "init",
2263
+ stepNumber: context.stepNumber,
2264
+ messages: [],
2265
+ // Child starts fresh
2266
+ expert: {
2267
+ key: expert.key,
2268
+ name: expert.name,
2269
+ version: expert.version
2270
+ },
2271
+ delegatedBy: {
2272
+ expert: {
2273
+ key: parentExpert.key,
2274
+ name: parentExpert.name,
2275
+ version: parentExpert.version
2276
+ },
2277
+ toolCallId,
2278
+ toolName,
2279
+ checkpointId: context.id
2280
+ },
2281
+ usage: context.usage,
2282
+ contextWindow: context.contextWindow,
2283
+ pendingToolCalls: void 0,
2284
+ partialToolResults: void 0
2285
+ };
2286
+ return { nextSetting, nextCheckpoint };
2287
+ }
2148
2288
  };
2149
- var noopRetrieveJob = () => void 0;
2150
- var noopRetrieveCheckpoint = async (_jobId, _checkpointId) => {
2151
- throw new Error("retrieveCheckpoint not provided");
2289
+ var ParallelDelegationStrategy = class {
2290
+ async execute(delegations, setting, context, parentExpert, runFn, parentOptions) {
2291
+ if (delegations.length < 2) {
2292
+ throw new Error("ParallelDelegationStrategy requires at least two delegations");
2293
+ }
2294
+ const [firstDelegation, ...remainingDelegations] = delegations;
2295
+ if (!firstDelegation) {
2296
+ throw new Error("No delegations found");
2297
+ }
2298
+ const allResults = await Promise.all(
2299
+ delegations.map(
2300
+ (delegation) => this.executeSingleDelegation(
2301
+ delegation,
2302
+ setting,
2303
+ context,
2304
+ parentExpert,
2305
+ runFn,
2306
+ parentOptions
2307
+ )
2308
+ )
2309
+ );
2310
+ const [firstResult, ...restResults] = allResults;
2311
+ if (!firstResult) {
2312
+ throw new Error("No delegation results");
2313
+ }
2314
+ const aggregatedUsage = allResults.reduce(
2315
+ (acc, result) => sumUsage(acc, result.deltaUsage),
2316
+ context.usage
2317
+ );
2318
+ const maxStepNumber = Math.max(...allResults.map((r) => r.stepNumber));
2319
+ const restToolResults = restResults.map((result) => ({
2320
+ id: result.toolCallId,
2321
+ skillName: `delegate/${result.expertKey}`,
2322
+ toolName: result.toolName,
2323
+ result: [{ type: "textPart", id: createId(), text: result.text }]
2324
+ }));
2325
+ const processedToolCallIds = new Set(remainingDelegations.map((d) => d.toolCallId));
2326
+ const remainingPendingToolCalls = context.pendingToolCalls?.filter(
2327
+ (tc) => !processedToolCallIds.has(tc.id) && tc.id !== firstDelegation.toolCallId
2328
+ );
2329
+ const nextSetting = {
2330
+ ...setting,
2331
+ expertKey: parentExpert.key,
2332
+ input: {
2333
+ interactiveToolCallResult: {
2334
+ toolCallId: firstResult.toolCallId,
2335
+ toolName: firstResult.toolName,
2336
+ skillName: `delegate/${firstResult.expertKey}`,
2337
+ text: firstResult.text
2338
+ }
2339
+ }
2340
+ };
2341
+ const nextCheckpoint = {
2342
+ id: context.id,
2343
+ jobId: setting.jobId,
2344
+ runId: setting.runId,
2345
+ status: "stoppedByDelegate",
2346
+ stepNumber: maxStepNumber,
2347
+ messages: context.messages,
2348
+ // Restore parent's conversation history
2349
+ expert: {
2350
+ key: parentExpert.key,
2351
+ name: parentExpert.name,
2352
+ version: parentExpert.version
2353
+ },
2354
+ usage: aggregatedUsage,
2355
+ contextWindow: context.contextWindow,
2356
+ delegatedBy: context.delegatedBy,
2357
+ // Preserve parent reference for nested delegations
2358
+ delegateTo: void 0,
2359
+ pendingToolCalls: remainingPendingToolCalls?.length ? remainingPendingToolCalls : void 0,
2360
+ partialToolResults: [...context.partialToolResults ?? [], ...restToolResults]
2361
+ };
2362
+ return { nextSetting, nextCheckpoint };
2363
+ }
2364
+ async executeSingleDelegation(delegation, parentSetting, parentContext, parentExpert, runFn, parentOptions) {
2365
+ const { expert, toolCallId, toolName, query } = delegation;
2366
+ const delegateRunId = createId();
2367
+ const delegateSetting = {
2368
+ ...parentSetting,
2369
+ runId: delegateRunId,
2370
+ expertKey: expert.key,
2371
+ input: { text: query }
2372
+ };
2373
+ const delegateCheckpoint = {
2374
+ id: createId(),
2375
+ jobId: parentSetting.jobId,
2376
+ runId: delegateRunId,
2377
+ status: "init",
2378
+ stepNumber: parentContext.stepNumber,
2379
+ messages: [],
2380
+ // Child starts fresh - no parent context inheritance
2381
+ expert: {
2382
+ key: expert.key,
2383
+ name: expert.name,
2384
+ version: expert.version
2385
+ },
2386
+ delegatedBy: {
2387
+ expert: {
2388
+ key: parentExpert.key,
2389
+ name: parentExpert.name,
2390
+ version: parentExpert.version
2391
+ },
2392
+ toolCallId,
2393
+ toolName,
2394
+ checkpointId: parentContext.id
2395
+ },
2396
+ usage: createEmptyUsage(),
2397
+ contextWindow: parentContext.contextWindow
2398
+ };
2399
+ const resultCheckpoint = await runFn(
2400
+ { setting: delegateSetting, checkpoint: delegateCheckpoint },
2401
+ { ...parentOptions, returnOnDelegationComplete: true }
2402
+ );
2403
+ return this.extractDelegationResult(resultCheckpoint, toolCallId, toolName, expert.key);
2404
+ }
2405
+ extractDelegationResult(checkpoint, toolCallId, toolName, expertKey) {
2406
+ const lastMessage = checkpoint.messages[checkpoint.messages.length - 1];
2407
+ if (!lastMessage || lastMessage.type !== "expertMessage") {
2408
+ throw new Error("Delegation error: delegation result message is incorrect");
2409
+ }
2410
+ const textPart = lastMessage.contents.find((c) => c.type === "textPart");
2411
+ if (!textPart || textPart.type !== "textPart") {
2412
+ throw new Error("Delegation error: delegation result message does not contain text");
2413
+ }
2414
+ return {
2415
+ toolCallId,
2416
+ toolName,
2417
+ expertKey,
2418
+ text: textPart.text,
2419
+ stepNumber: checkpoint.stepNumber,
2420
+ deltaUsage: checkpoint.usage
2421
+ };
2422
+ }
2152
2423
  };
2153
- var defaultCreateJob = (jobId, expertKey, maxSteps) => ({
2154
- id: jobId,
2155
- coordinatorExpertKey: expertKey,
2156
- status: "running",
2157
- totalSteps: 0,
2158
- startedAt: Date.now(),
2159
- maxSteps,
2160
- usage: createEmptyUsage()
2161
- });
2162
- async function run(runInput, options) {
2163
- const runParams = runParamsSchema.parse(runInput);
2164
- const storeCheckpoint = options?.storeCheckpoint ?? noopStoreCheckpoint;
2165
- const storeEvent = options?.storeEvent ?? noopStoreEvent;
2166
- const storeJob = options?.storeJob ?? noopStoreJob;
2167
- const retrieveJob = options?.retrieveJob ?? noopRetrieveJob;
2168
- const retrieveCheckpoint = options?.retrieveCheckpoint ?? noopRetrieveCheckpoint;
2169
- const createJob = options?.createJob ?? defaultCreateJob;
2170
- const eventListener = createEventListener(options?.eventListener, storeEvent);
2171
- const eventEmitter = new RunEventEmitter();
2172
- eventEmitter.subscribe(eventListener);
2173
- let { setting, checkpoint } = runParams;
2174
- const contextWindow = getContextWindow(setting.providerConfig.providerName, setting.model);
2175
- let job = retrieveJob(setting.jobId) ?? createJob(setting.jobId, setting.expertKey, setting.maxSteps);
2176
- if (job.status !== "running") {
2177
- job = { ...job, status: "running", finishedAt: void 0 };
2424
+ function selectDelegationStrategy(delegationCount) {
2425
+ if (delegationCount === 1) {
2426
+ return new SingleDelegationStrategy();
2178
2427
  }
2179
- storeJob(job);
2180
- while (true) {
2181
- const { expertToRun, experts } = await setupExperts(setting, options?.resolveExpertToRun);
2182
- if (options?.eventListener) {
2183
- const initEvent = createRuntimeEvent("initializeRuntime", setting.jobId, setting.runId, {
2184
- runtimeVersion: package_default.version,
2185
- runtime: "local",
2186
- expertName: expertToRun.name,
2187
- experts: Object.keys(experts),
2188
- model: setting.model,
2189
- temperature: setting.temperature,
2190
- maxSteps: setting.maxSteps,
2191
- maxRetries: setting.maxRetries,
2192
- timeout: setting.timeout,
2193
- query: setting.input.text,
2194
- interactiveToolCall: setting.input.interactiveToolCallResult
2195
- });
2196
- options.eventListener(initEvent);
2428
+ return new ParallelDelegationStrategy();
2429
+ }
2430
+ function buildReturnFromDelegation(currentSetting, resultCheckpoint, parentCheckpoint) {
2431
+ return buildDelegationReturnState(currentSetting, resultCheckpoint, parentCheckpoint);
2432
+ }
2433
+ function extractDelegationContext(checkpoint) {
2434
+ return {
2435
+ id: checkpoint.id,
2436
+ stepNumber: checkpoint.stepNumber,
2437
+ contextWindow: checkpoint.contextWindow,
2438
+ usage: checkpoint.usage,
2439
+ pendingToolCalls: checkpoint.pendingToolCalls,
2440
+ partialToolResults: checkpoint.partialToolResults,
2441
+ delegatedBy: checkpoint.delegatedBy,
2442
+ // Preserve for nested delegations
2443
+ messages: checkpoint.messages
2444
+ // Preserve for parent continuation after delegation
2445
+ };
2446
+ }
2447
+ var RunEventEmitter = class {
2448
+ listeners = [];
2449
+ subscribe(listener) {
2450
+ this.listeners.push(listener);
2451
+ }
2452
+ async emit(event) {
2453
+ const errors = [];
2454
+ for (const listener of this.listeners) {
2455
+ try {
2456
+ await listener({
2457
+ ...event,
2458
+ id: createId(),
2459
+ timestamp: Date.now()
2460
+ });
2461
+ } catch (error) {
2462
+ errors.push(error instanceof Error ? error : new Error(String(error)));
2463
+ }
2464
+ }
2465
+ if (errors.length > 0) {
2466
+ throw new AggregateError(errors, "One or more event listeners failed");
2197
2467
  }
2468
+ }
2469
+ };
2470
+
2471
+ // src/orchestration/single-run-executor.ts
2472
+ var SingleRunExecutor = class {
2473
+ constructor(options = {}) {
2474
+ this.options = options;
2475
+ }
2476
+ async execute(setting, checkpoint) {
2477
+ const contextWindow = getContextWindow(setting.providerConfig.providerName, setting.model);
2478
+ const { expertToRun, experts } = await setupExperts(setting, this.options.resolveExpertToRun);
2479
+ this.emitInitEvent(setting, expertToRun, experts);
2198
2480
  const skillManagers = await getSkillManagers(
2199
2481
  expertToRun,
2200
2482
  experts,
2201
2483
  setting,
2202
- options?.eventListener,
2484
+ this.options.eventListener,
2203
2485
  { isDelegatedRun: !!checkpoint?.delegatedBy }
2204
2486
  );
2205
2487
  const initialCheckpoint = checkpoint ? createNextStepCheckpoint(createId(), checkpoint) : createInitialCheckpoint(createId(), {
@@ -2209,183 +2491,148 @@ async function run(runInput, options) {
2209
2491
  expert: expertToRun,
2210
2492
  contextWindow
2211
2493
  });
2212
- const runResultCheckpoint = await executeStateMachine({
2494
+ const eventEmitter = new RunEventEmitter();
2495
+ const eventListener = this.createEventListener();
2496
+ eventEmitter.subscribe(eventListener);
2497
+ const resultCheckpoint = await executeStateMachine({
2213
2498
  setting: { ...setting, experts },
2214
2499
  initialCheckpoint,
2215
2500
  eventListener,
2216
2501
  skillManagers,
2217
2502
  eventEmitter,
2218
- storeCheckpoint,
2219
- shouldContinueRun: options?.shouldContinueRun
2503
+ storeCheckpoint: this.options.storeCheckpoint ?? (async () => {
2504
+ }),
2505
+ shouldContinueRun: this.options.shouldContinueRun
2506
+ });
2507
+ return { checkpoint: resultCheckpoint, expertToRun, experts };
2508
+ }
2509
+ createEventListener() {
2510
+ const userListener = this.options.eventListener;
2511
+ const storeEvent = this.options.storeEvent;
2512
+ return async (event) => {
2513
+ if ("stepNumber" in event && storeEvent) {
2514
+ await storeEvent(event);
2515
+ }
2516
+ userListener?.(event);
2517
+ };
2518
+ }
2519
+ emitInitEvent(setting, expertToRun, experts) {
2520
+ if (!this.options.eventListener) return;
2521
+ const initEvent = createRuntimeEvent("initializeRuntime", setting.jobId, setting.runId, {
2522
+ runtimeVersion: package_default.version,
2523
+ runtime: "local",
2524
+ expertName: expertToRun.name,
2525
+ experts: Object.keys(experts),
2526
+ model: setting.model,
2527
+ temperature: setting.temperature,
2528
+ maxSteps: setting.maxSteps,
2529
+ maxRetries: setting.maxRetries,
2530
+ timeout: setting.timeout,
2531
+ query: setting.input.text,
2532
+ interactiveToolCall: setting.input.interactiveToolCallResult
2220
2533
  });
2534
+ this.options.eventListener(initEvent);
2535
+ }
2536
+ };
2537
+
2538
+ // src/run.ts
2539
+ var defaultCreateJob = (jobId, expertKey, maxSteps) => ({
2540
+ id: jobId,
2541
+ coordinatorExpertKey: expertKey,
2542
+ status: "running",
2543
+ totalSteps: 0,
2544
+ startedAt: Date.now(),
2545
+ maxSteps,
2546
+ usage: createEmptyUsage()
2547
+ });
2548
+ async function run(runInput, options) {
2549
+ const runParams = runParamsSchema.parse(runInput);
2550
+ let { setting, checkpoint } = runParams;
2551
+ const storeJob = options?.storeJob ?? (() => {
2552
+ });
2553
+ const retrieveJob = options?.retrieveJob ?? (() => void 0);
2554
+ const retrieveCheckpoint = options?.retrieveCheckpoint ?? (async () => {
2555
+ throw new Error("retrieveCheckpoint not provided");
2556
+ });
2557
+ const createJob = options?.createJob ?? defaultCreateJob;
2558
+ let job = retrieveJob(setting.jobId) ?? createJob(setting.jobId, setting.expertKey, setting.maxSteps);
2559
+ if (job.status !== "running") {
2560
+ job = { ...job, status: "running", finishedAt: void 0 };
2561
+ }
2562
+ storeJob(job);
2563
+ const runExecutor = new SingleRunExecutor({
2564
+ shouldContinueRun: options?.shouldContinueRun,
2565
+ storeCheckpoint: options?.storeCheckpoint,
2566
+ storeEvent: options?.storeEvent,
2567
+ eventListener: options?.eventListener,
2568
+ resolveExpertToRun: options?.resolveExpertToRun
2569
+ });
2570
+ while (true) {
2571
+ const runResult = await runExecutor.execute(setting, checkpoint);
2572
+ const resultCheckpoint = runResult.checkpoint;
2221
2573
  job = {
2222
2574
  ...job,
2223
- totalSteps: runResultCheckpoint.stepNumber,
2224
- usage: runResultCheckpoint.usage
2575
+ totalSteps: resultCheckpoint.stepNumber,
2576
+ usage: resultCheckpoint.usage
2225
2577
  };
2226
- switch (runResultCheckpoint.status) {
2578
+ switch (resultCheckpoint.status) {
2227
2579
  case "completed": {
2228
2580
  if (options?.returnOnDelegationComplete) {
2229
2581
  storeJob(job);
2230
- return runResultCheckpoint;
2582
+ return resultCheckpoint;
2231
2583
  }
2232
- if (runResultCheckpoint.delegatedBy) {
2584
+ if (resultCheckpoint.delegatedBy) {
2233
2585
  storeJob(job);
2234
2586
  const parentCheckpoint = await retrieveCheckpoint(
2235
2587
  setting.jobId,
2236
- runResultCheckpoint.delegatedBy.checkpointId
2588
+ resultCheckpoint.delegatedBy.checkpointId
2237
2589
  );
2238
- const result = buildDelegationReturnState(setting, runResultCheckpoint, parentCheckpoint);
2590
+ const result = buildReturnFromDelegation(setting, resultCheckpoint, parentCheckpoint);
2239
2591
  setting = result.setting;
2240
2592
  checkpoint = result.checkpoint;
2241
2593
  break;
2242
2594
  }
2243
2595
  storeJob({ ...job, status: "completed", finishedAt: Date.now() });
2244
- return runResultCheckpoint;
2596
+ return resultCheckpoint;
2245
2597
  }
2246
2598
  case "stoppedByInteractiveTool": {
2247
2599
  storeJob({ ...job, status: "stoppedByInteractiveTool" });
2248
- return runResultCheckpoint;
2600
+ return resultCheckpoint;
2249
2601
  }
2250
2602
  case "stoppedByDelegate": {
2251
2603
  storeJob(job);
2252
- const { delegateTo } = runResultCheckpoint;
2604
+ const { delegateTo } = resultCheckpoint;
2253
2605
  if (!delegateTo || delegateTo.length === 0) {
2254
2606
  throw new Error("No delegations found in checkpoint");
2255
2607
  }
2256
- if (delegateTo.length === 1) {
2257
- const result = buildDelegateToState(setting, runResultCheckpoint, expertToRun);
2258
- setting = result.setting;
2259
- checkpoint = result.checkpoint;
2260
- break;
2261
- }
2262
- const firstDelegation = delegateTo[0];
2263
- const remainingDelegations = delegateTo.slice(1);
2264
- const [firstResult, ...restResults] = await Promise.all(
2265
- delegateTo.map(
2266
- (delegation) => runDelegate(delegation, setting, runResultCheckpoint, expertToRun, options)
2267
- )
2608
+ const strategy = selectDelegationStrategy(delegateTo.length);
2609
+ const context = extractDelegationContext(resultCheckpoint);
2610
+ const delegationResult = await strategy.execute(
2611
+ delegateTo,
2612
+ setting,
2613
+ context,
2614
+ runResult.expertToRun,
2615
+ run,
2616
+ options
2268
2617
  );
2269
- const allResults = [firstResult, ...restResults];
2270
- const aggregatedUsage = allResults.reduce(
2271
- (acc, result) => sumUsage(acc, result.deltaUsage),
2272
- runResultCheckpoint.usage
2273
- );
2274
- const maxStepNumber = Math.max(...allResults.map((r) => r.stepNumber));
2275
- const restToolResults = restResults.map((result) => ({
2276
- id: result.toolCallId,
2277
- skillName: `delegate/${result.expertKey}`,
2278
- toolName: result.toolName,
2279
- result: [{ type: "textPart", id: createId(), text: result.text }]
2280
- }));
2281
- const processedToolCallIds = new Set(remainingDelegations.map((d) => d.toolCallId));
2282
- const remainingToolCalls = runResultCheckpoint.pendingToolCalls?.filter(
2283
- (tc) => !processedToolCallIds.has(tc.id) && tc.id !== firstDelegation.toolCallId
2284
- );
2285
- setting = {
2286
- ...setting,
2287
- expertKey: expertToRun.key,
2288
- input: {
2289
- interactiveToolCallResult: {
2290
- toolCallId: firstResult.toolCallId,
2291
- toolName: firstResult.toolName,
2292
- skillName: `delegate/${firstResult.expertKey}`,
2293
- text: firstResult.text
2294
- }
2295
- }
2296
- };
2297
- checkpoint = {
2298
- ...runResultCheckpoint,
2299
- status: "stoppedByDelegate",
2300
- delegateTo: void 0,
2301
- stepNumber: maxStepNumber,
2302
- usage: aggregatedUsage,
2303
- pendingToolCalls: remainingToolCalls?.length ? remainingToolCalls : void 0,
2304
- partialToolResults: [
2305
- ...runResultCheckpoint.partialToolResults ?? [],
2306
- ...restToolResults
2307
- ]
2308
- };
2618
+ setting = delegationResult.nextSetting;
2619
+ checkpoint = delegationResult.nextCheckpoint;
2309
2620
  break;
2310
2621
  }
2311
2622
  case "stoppedByExceededMaxSteps": {
2312
2623
  storeJob({ ...job, status: "stoppedByMaxSteps", finishedAt: Date.now() });
2313
- return runResultCheckpoint;
2624
+ return resultCheckpoint;
2314
2625
  }
2315
2626
  case "stoppedByError": {
2316
2627
  storeJob({ ...job, status: "stoppedByError", finishedAt: Date.now() });
2317
- return runResultCheckpoint;
2628
+ return resultCheckpoint;
2318
2629
  }
2319
2630
  default:
2320
2631
  throw new Error("Run stopped by unknown reason");
2321
2632
  }
2322
2633
  }
2323
2634
  }
2324
- function createEventListener(userListener, storeEvent) {
2325
- const listener = userListener ?? ((e) => console.log(JSON.stringify(e)));
2326
- return async (event) => {
2327
- if ("stepNumber" in event && storeEvent) {
2328
- await storeEvent(event);
2329
- }
2330
- listener(event);
2331
- };
2332
- }
2333
- async function runDelegate(delegation, parentSetting, parentCheckpoint, parentExpert, options) {
2334
- const { expert, toolCallId, toolName, query } = delegation;
2335
- const delegateRunId = createId();
2336
- const delegateSetting = {
2337
- ...parentSetting,
2338
- runId: delegateRunId,
2339
- expertKey: expert.key,
2340
- input: { text: query }
2341
- };
2342
- const delegateCheckpoint = {
2343
- id: createId(),
2344
- jobId: parentSetting.jobId,
2345
- runId: delegateRunId,
2346
- status: "init",
2347
- stepNumber: parentCheckpoint.stepNumber,
2348
- messages: [],
2349
- expert: {
2350
- key: expert.key,
2351
- name: expert.name,
2352
- version: expert.version
2353
- },
2354
- delegatedBy: {
2355
- expert: {
2356
- key: parentExpert.key,
2357
- name: parentExpert.name,
2358
- version: parentExpert.version
2359
- },
2360
- toolCallId,
2361
- toolName,
2362
- checkpointId: parentCheckpoint.id
2363
- },
2364
- usage: createEmptyUsage(),
2365
- contextWindow: parentCheckpoint.contextWindow
2366
- };
2367
- const resultCheckpoint = await run(
2368
- { setting: delegateSetting, checkpoint: delegateCheckpoint },
2369
- { ...options, returnOnDelegationComplete: true }
2370
- );
2371
- const lastMessage = resultCheckpoint.messages[resultCheckpoint.messages.length - 1];
2372
- if (!lastMessage || lastMessage.type !== "expertMessage") {
2373
- throw new Error("Delegation error: delegation result message is incorrect");
2374
- }
2375
- const textPart = lastMessage.contents.find((c) => c.type === "textPart");
2376
- if (!textPart || textPart.type !== "textPart") {
2377
- throw new Error("Delegation error: delegation result message does not contain text");
2378
- }
2379
- return {
2380
- toolCallId,
2381
- toolName,
2382
- expertKey: expert.key,
2383
- text: textPart.text,
2384
- stepNumber: resultCheckpoint.stepNumber,
2385
- deltaUsage: resultCheckpoint.usage
2386
- };
2387
- }
2388
2635
 
2389
2636
  export { getModel, package_default, run, runtimeStateMachine };
2390
- //# sourceMappingURL=chunk-AQSRQW5R.js.map
2391
- //# sourceMappingURL=chunk-AQSRQW5R.js.map
2637
+ //# sourceMappingURL=chunk-H65LPOAK.js.map
2638
+ //# sourceMappingURL=chunk-H65LPOAK.js.map