@rowan-agent/agent 0.4.11 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,28 +1,3 @@
1
- import {
2
- buildPhaseDirectiveMessage,
3
- buildSkillsDescription,
4
- buildStructuredSection,
5
- detectResourceType,
6
- formatResourceOutput,
7
- inferResourceName,
8
- interpolateEnvVars,
9
- loadConfigFile,
10
- loadMarkdown,
11
- loadPhases,
12
- parseFrontmatter,
13
- parseModelRef,
14
- readPhaseContent,
15
- registerConfigModels,
16
- reloadPhases,
17
- resolveDefaultModel,
18
- resolveResourcePath
19
- } from "./chunk-FTIS43EE.js";
20
- import {
21
- normalizeRelativePath,
22
- resolveInWorkspace,
23
- resolveWorkspacePaths
24
- } from "./chunk-2FIII4GP.js";
25
-
26
1
  // src/utils.ts
27
2
  function createId(prefix) {
28
3
  return `${prefix}_${crypto.randomUUID().slice(0, 8)}`;
@@ -46,6 +21,19 @@ var EmptyResponseError = class extends Error {
46
21
  this.name = "EmptyResponseError";
47
22
  }
48
23
  };
24
+ var MissingRouteToolCallError = class extends Error {
25
+ code = "missing_route_tool_call";
26
+ phase;
27
+ toolCallNames;
28
+ constructor(phase, toolCallNames) {
29
+ super(
30
+ `Phase "${phase}" requires a route tool call, but the model did not emit one.`
31
+ );
32
+ this.name = "MissingRouteToolCallError";
33
+ this.phase = phase;
34
+ this.toolCallNames = toolCallNames;
35
+ }
36
+ };
49
37
  var LoopGuard = {
50
38
  /** Returns abort result if signal is aborted */
51
39
  checkAbort(signal) {
@@ -379,12 +367,596 @@ function createExtensionAPI(hooks, _extensionPath, options, runtime, eventBus) {
379
367
  };
380
368
  }
381
369
 
370
+ // src/harness/phases/loader.ts
371
+ import { existsSync as existsSync2, statSync } from "fs";
372
+ import { readdir } from "fs/promises";
373
+ import { dirname as dirname2, join as join2, resolve } from "path";
374
+
375
+ // src/harness/loader.ts
376
+ import { readFile } from "fs/promises";
377
+ import { basename, dirname, extname } from "path";
378
+ function parseFrontmatter(raw) {
379
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
380
+ if (!match) return { frontmatter: {}, body: raw };
381
+ const lines = match[1].split("\n");
382
+ const frontmatter = {};
383
+ for (let i = 0; i < lines.length; i++) {
384
+ const line = lines[i];
385
+ const idx = line.indexOf(":");
386
+ if (idx === -1) continue;
387
+ const key = line.slice(0, idx).trim();
388
+ const value = line.slice(idx + 1).trim();
389
+ if (!key) continue;
390
+ if (value === "" && i + 1 < lines.length) {
391
+ const nested = {};
392
+ while (i + 1 < lines.length) {
393
+ const next = lines[i + 1];
394
+ if (!next.startsWith(" ") || next.trim() === "") break;
395
+ const nIdx = next.indexOf(":");
396
+ if (nIdx === -1) break;
397
+ const nKey = next.slice(0, nIdx).trim();
398
+ const nValue = next.slice(nIdx + 1).trim();
399
+ nested[nKey] = nValue;
400
+ i++;
401
+ }
402
+ if (Object.keys(nested).length) {
403
+ frontmatter[key] = nested;
404
+ }
405
+ continue;
406
+ }
407
+ frontmatter[key] = parseValue(value);
408
+ }
409
+ return { frontmatter, body: raw.slice(match[0].length) };
410
+ }
411
+ function parseValue(value) {
412
+ const arrayMatch = value.match(/^\[(.*)\]$/);
413
+ if (arrayMatch) {
414
+ return arrayMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
415
+ }
416
+ if (value.toLowerCase() === "true") return true;
417
+ if (value.toLowerCase() === "false") return false;
418
+ if (/^\d+$/.test(value)) return Number(value);
419
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
420
+ return value.slice(1, -1);
421
+ }
422
+ return value;
423
+ }
424
+ async function loadMarkdown(filePath) {
425
+ const raw = await readFile(filePath, "utf8");
426
+ const { frontmatter, body } = parseFrontmatter(raw);
427
+ return { frontmatter, body, raw };
428
+ }
429
+ function inferResourceName(path, markerFile) {
430
+ const file = basename(path);
431
+ if (file.toLowerCase() === markerFile.toLowerCase()) {
432
+ return basename(dirname(path));
433
+ }
434
+ const extension = extname(file);
435
+ return extension ? file.slice(0, -extension.length) : file;
436
+ }
437
+
438
+ // src/harness/config.ts
439
+ import { existsSync } from "fs";
440
+ import { readFile as readFile2 } from "fs/promises";
441
+ import { join } from "path";
442
+ import { parse as parseYaml } from "yaml";
443
+ import { registerModel } from "@rowan-agent/models";
444
+ var DEFAULT_REASONING = false;
445
+ var DEFAULT_INPUT = ["text"];
446
+ var DEFAULT_CONTEXT_WINDOW = 128e3;
447
+ var DEFAULT_MAX_TOKENS = 16384;
448
+ var DEFAULT_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
449
+ var DEFAULT_TIMEOUT_MS = 6e4;
450
+ var ENV_VAR_PATTERN = /\$\{([^}]+)\}/g;
451
+ function interpolateEnvVars(value) {
452
+ return value.replace(ENV_VAR_PATTERN, (match, varName) => {
453
+ const resolved = process.env[varName];
454
+ if (resolved === void 0 || resolved === "") {
455
+ throw new Error(
456
+ `Environment variable "${varName}" is not set but is referenced in config.yaml via "${match}".`
457
+ );
458
+ }
459
+ return resolved;
460
+ });
461
+ }
462
+ var CONFIG_CANDIDATES = ["config.yaml", "config.yml"];
463
+ async function loadConfigFile(workspace) {
464
+ let configPath;
465
+ let configFilename;
466
+ for (const candidate of CONFIG_CANDIDATES) {
467
+ const p = join(workspace.rowanDir, candidate);
468
+ if (existsSync(p)) {
469
+ configPath = p;
470
+ configFilename = candidate;
471
+ break;
472
+ }
473
+ }
474
+ if (!configPath || !configFilename) {
475
+ return void 0;
476
+ }
477
+ const raw = await readFile2(configPath, "utf8");
478
+ let parsed;
479
+ try {
480
+ parsed = parseYaml(raw);
481
+ } catch (error) {
482
+ const message = error instanceof Error ? error.message : String(error);
483
+ throw new Error(`Failed to parse .rowan/${configFilename}: ${message}`);
484
+ }
485
+ if (parsed === null || parsed === void 0) {
486
+ return void 0;
487
+ }
488
+ return validateConfigFile(parsed, configFilename);
489
+ }
490
+ function validateConfigFile(parsed, configFilename) {
491
+ if (typeof parsed !== "object" || parsed === null) {
492
+ throw new Error(`.rowan/${configFilename} must be a YAML object at the top level.`);
493
+ }
494
+ const obj = parsed;
495
+ const providers = obj.providers;
496
+ if (!Array.isArray(providers)) {
497
+ throw new Error(`.rowan/${configFilename} must have a "providers" array.`);
498
+ }
499
+ const validatedProviders = providers.map((p, i) => {
500
+ if (typeof p !== "object" || p === null) {
501
+ throw new Error(`providers[${i}] must be an object.`);
502
+ }
503
+ const provider = p;
504
+ if (typeof provider.id !== "string" || !provider.id) {
505
+ throw new Error(`providers[${i}].id is required and must be a non-empty string.`);
506
+ }
507
+ if (typeof provider.baseUrl !== "string" || !provider.baseUrl) {
508
+ throw new Error(`providers[${i}].baseUrl is required and must be a non-empty string.`);
509
+ }
510
+ if (typeof provider.apiKey !== "string" || !provider.apiKey) {
511
+ throw new Error(`providers[${i}].apiKey is required and must be a non-empty string.`);
512
+ }
513
+ if (typeof provider.protocol !== "string" || !provider.protocol) {
514
+ throw new Error(`providers[${i}].protocol is required and must be a non-empty string.`);
515
+ }
516
+ if (!Array.isArray(provider.models) || provider.models.length === 0) {
517
+ throw new Error(`providers[${i}].models must be a non-empty array.`);
518
+ }
519
+ const interpolatedApiKey = interpolateEnvVars(provider.apiKey);
520
+ const validatedModels = provider.models.map((m, j) => {
521
+ if (typeof m !== "object" || m === null) {
522
+ throw new Error(`providers[${i}].models[${j}] must be an object.`);
523
+ }
524
+ const model = m;
525
+ if (typeof model.id !== "string" || !model.id) {
526
+ throw new Error(`providers[${i}].models[${j}].id is required and must be a non-empty string.`);
527
+ }
528
+ return model;
529
+ });
530
+ return {
531
+ id: provider.id,
532
+ baseUrl: provider.baseUrl,
533
+ apiKey: interpolatedApiKey,
534
+ protocol: provider.protocol,
535
+ ...typeof provider.name === "string" ? { name: provider.name } : {},
536
+ ...typeof provider.timeoutMs === "number" ? { timeoutMs: provider.timeoutMs } : {},
537
+ ...typeof provider.maxRetries === "number" ? { maxRetries: provider.maxRetries } : {},
538
+ ...typeof provider.retryDelayMs === "number" ? { retryDelayMs: provider.retryDelayMs } : {},
539
+ ...provider.headers && typeof provider.headers === "object" ? { headers: provider.headers } : {},
540
+ models: validatedModels
541
+ };
542
+ });
543
+ const result = { providers: validatedProviders };
544
+ if (obj.model && typeof obj.model === "object") {
545
+ const modelRef = obj.model;
546
+ if (typeof modelRef.provider !== "string" || typeof modelRef.id !== "string") {
547
+ throw new Error(`.rowan/${configFilename} "model" must have "provider" and "id" string fields.`);
548
+ }
549
+ result.model = { provider: modelRef.provider, id: modelRef.id };
550
+ }
551
+ if (obj.logLevel !== void 0) {
552
+ const valid = ["debug", "info", "warn", "error", "silent"];
553
+ if (typeof obj.logLevel !== "string" || !valid.includes(obj.logLevel)) {
554
+ throw new Error(`.rowan/${configFilename} "logLevel" must be one of: ${valid.join(", ")}.`);
555
+ }
556
+ result.logLevel = obj.logLevel;
557
+ }
558
+ return result;
559
+ }
560
+ function resolveDefaultModel(config) {
561
+ if (config.model) {
562
+ return { provider: config.model.provider, id: config.model.id };
563
+ }
564
+ for (const provider of config.providers) {
565
+ for (const model of provider.models) {
566
+ if (model.primary) {
567
+ return { provider: provider.id, id: model.id };
568
+ }
569
+ }
570
+ }
571
+ for (const provider of config.providers) {
572
+ if (provider.models.length > 0) {
573
+ return { provider: provider.id, id: provider.models[0].id };
574
+ }
575
+ }
576
+ return void 0;
577
+ }
578
+ function registerConfigModels(config) {
579
+ for (const provider of config.providers) {
580
+ for (const fileModel of provider.models) {
581
+ const model = {
582
+ id: fileModel.id,
583
+ name: fileModel.name ?? fileModel.id,
584
+ protocol: provider.protocol,
585
+ provider: provider.id,
586
+ baseUrl: provider.baseUrl,
587
+ reasoning: fileModel.reasoning ?? DEFAULT_REASONING,
588
+ input: fileModel.input ?? DEFAULT_INPUT,
589
+ cost: { ...DEFAULT_COST, ...fileModel.cost },
590
+ contextWindow: fileModel.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
591
+ maxTokens: fileModel.maxTokens ?? DEFAULT_MAX_TOKENS,
592
+ apiKey: provider.apiKey,
593
+ timeoutMs: provider.timeoutMs ?? DEFAULT_TIMEOUT_MS,
594
+ ...provider.maxRetries !== void 0 ? { maxRetries: provider.maxRetries } : {},
595
+ ...provider.retryDelayMs !== void 0 ? { retryDelayMs: provider.retryDelayMs } : {},
596
+ ...provider.headers ? { headers: provider.headers } : {}
597
+ };
598
+ registerModel(model);
599
+ }
600
+ }
601
+ }
602
+ function parseModelRef(input) {
603
+ if (!input) return void 0;
604
+ const slashIndex = input.indexOf("/");
605
+ if (slashIndex === -1) {
606
+ return { provider: "*", id: input };
607
+ }
608
+ return {
609
+ provider: input.slice(0, slashIndex),
610
+ id: input.slice(slashIndex + 1)
611
+ };
612
+ }
613
+
614
+ // src/harness/context/resource-formatter.ts
615
+ function escapeXml(value) {
616
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
617
+ }
618
+ function buildStructuredSection(tag, items) {
619
+ const lines = [];
620
+ for (const item of items) {
621
+ lines.push(` <${tag}>`);
622
+ for (const [key, value] of Object.entries(item)) {
623
+ lines.push(` <${key}>${escapeXml(value)}</${key}>`);
624
+ }
625
+ lines.push(` </${tag}>`);
626
+ }
627
+ return lines.join("\n");
628
+ }
629
+ function buildSkillsDescription(skills) {
630
+ const visibleSkills = skills.filter((skill) => !skill.disableModelInvocation);
631
+ if (visibleSkills.length === 0) return "";
632
+ const lines = ["<available_skills>"];
633
+ for (const skill of visibleSkills) {
634
+ lines.push(" <skill>");
635
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
636
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
637
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
638
+ lines.push(" </skill>");
639
+ }
640
+ lines.push("</available_skills>");
641
+ return lines.join("\n");
642
+ }
643
+ function formatResourceOutput(resource) {
644
+ const parts = [`<${resource.type} name="${escapeXml(resource.name)}" location="${escapeXml(resource.location)}">`];
645
+ if (resource.baseDir) {
646
+ parts.push(`References are relative to ${resource.baseDir}.`);
647
+ parts.push("");
648
+ }
649
+ parts.push(resource.content);
650
+ parts.push(`</${resource.type}>`);
651
+ return parts.join("\n");
652
+ }
653
+ function detectResourceType(filePath) {
654
+ const normalized = filePath.replace(/\\/g, "/");
655
+ if (normalized.includes("/.rowan/skills/") && normalized.endsWith("/SKILL.md")) return "skill";
656
+ if (normalized.includes("/.rowan/phases/") && normalized.endsWith("/PHASE.md")) return "phase";
657
+ if (normalized.endsWith(".md")) return "markdown";
658
+ if (/\.(ts|js|tsx|jsx|py|rs|go|java|c|cpp|rb|sh|sql|yaml|yml|json|toml)$/.test(normalized)) return "code";
659
+ return "file";
660
+ }
661
+ function jsonToXml(value, depth) {
662
+ const indent = " ".repeat(depth);
663
+ if (value === null || value === void 0) return "";
664
+ if (typeof value !== "object") return `${indent}${escapeXml(String(value))}`;
665
+ if (Array.isArray(value)) {
666
+ return value.map((item) => {
667
+ if (typeof item === "object" && item !== null) {
668
+ return `${indent}<item>
669
+ ${jsonToXml(item, depth + 1)}
670
+ ${indent}</item>`;
671
+ }
672
+ return `${indent}<item>${escapeXml(String(item))}</item>`;
673
+ }).join("\n");
674
+ }
675
+ return Object.entries(value).map(([key, val]) => {
676
+ const tag = escapeXml(key);
677
+ if (typeof val === "object" && val !== null) {
678
+ return `${indent}<${tag}>
679
+ ${jsonToXml(val, depth + 1)}
680
+ ${indent}</${tag}>`;
681
+ }
682
+ return `${indent}<${tag}>${escapeXml(String(val))}</${tag}>`;
683
+ }).join("\n");
684
+ }
685
+ function buildPhaseDirectiveMessage(phase, output, toolUseId) {
686
+ const parts = [];
687
+ parts.push(`<phase name="${escapeXml(phase.name)}">`);
688
+ parts.push(` <content>${phase.content}</content>`);
689
+ if (output.results && output.results.length > 0) {
690
+ parts.push(` <prev_phase_outputs>`);
691
+ if (output.instruction) {
692
+ parts.push(` <instruction>${escapeXml(output.instruction)}</instruction>`);
693
+ }
694
+ for (const r of output.results) {
695
+ parts.push(` <phase name="${escapeXml(r.name)}">`);
696
+ if (r.output !== void 0) {
697
+ parts.push(jsonToXml(r.output, 3));
698
+ }
699
+ parts.push(` </phase>`);
700
+ }
701
+ parts.push(` </prev_phase_outputs>`);
702
+ }
703
+ parts.push(`</phase>`);
704
+ return [{
705
+ type: "tool_result",
706
+ toolUseId,
707
+ content: parts.join("\n"),
708
+ isError: false
709
+ }];
710
+ }
711
+
712
+ // src/harness/phases/loader.ts
713
+ var PHASE_MARKER = "PHASE.md";
714
+ function resolvePhasePath(input) {
715
+ const resolved = resolve(input);
716
+ return existsSync2(resolved) && statSync(resolved).isDirectory() ? join2(resolved, PHASE_MARKER) : resolved;
717
+ }
718
+ async function loadPhase(targetPath) {
719
+ const resolved = resolvePhasePath(targetPath);
720
+ const { frontmatter, body } = await loadMarkdown(resolved);
721
+ const id = inferResourceName(resolved, PHASE_MARKER);
722
+ if (!frontmatter.name) {
723
+ throw new Error(`Phase "${id}" at "${resolved}" is missing required field "name" in PHASE.md frontmatter.`);
724
+ }
725
+ if (!frontmatter.description) {
726
+ throw new Error(`Phase "${id}" at "${resolved}" is missing required field "description" in PHASE.md frontmatter.`);
727
+ }
728
+ const baseDir = dirname2(resolved);
729
+ const phase = {
730
+ id,
731
+ name: frontmatter.name,
732
+ description: frontmatter.description,
733
+ tools: frontmatter.tools,
734
+ skills: frontmatter.skills,
735
+ target: frontmatter.target,
736
+ input: frontmatter.input,
737
+ isolated: frontmatter.isolated,
738
+ filePath: resolved,
739
+ baseDir,
740
+ content: body,
741
+ model: parseModelRef(frontmatter.model)
742
+ };
743
+ const codePath = await discoverPhaseCode(baseDir);
744
+ if (codePath) {
745
+ const code = await loadPhaseCode(codePath);
746
+ if (code.factory) {
747
+ phase.factory = code.factory;
748
+ } else if (code.run) {
749
+ phase.run = code.run;
750
+ }
751
+ }
752
+ return phase;
753
+ }
754
+ function readPhaseContent(phase) {
755
+ return formatResourceOutput({
756
+ type: "phase",
757
+ name: phase.name,
758
+ location: phase.filePath,
759
+ content: phase.content,
760
+ baseDir: phase.baseDir
761
+ });
762
+ }
763
+ function resolvePhaseLoadTarget(targetPath) {
764
+ return typeof targetPath === "string" ? targetPath : join2(targetPath.rowanDir, "phases");
765
+ }
766
+ async function loadPhases(targetPath) {
767
+ const phases = /* @__PURE__ */ new Map();
768
+ const phasesDir = resolve(resolvePhaseLoadTarget(targetPath));
769
+ if (existsSync2(phasesDir) && statSync(phasesDir).isFile()) {
770
+ const phase = await loadPhase(phasesDir);
771
+ phases.set(phase.id, phase);
772
+ return { phases, entryPhaseId: null };
773
+ }
774
+ if (existsSync2(join2(phasesDir, PHASE_MARKER))) {
775
+ const phase = await loadPhase(phasesDir);
776
+ phases.set(phase.id, phase);
777
+ return { phases, entryPhaseId: null };
778
+ }
779
+ const entries = await readdir(phasesDir, { withFileTypes: true });
780
+ for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
781
+ if (!entry.isDirectory()) continue;
782
+ const phaseFile = join2(phasesDir, entry.name, PHASE_MARKER);
783
+ if (!existsSync2(phaseFile)) continue;
784
+ try {
785
+ const phase = await loadPhase(phaseFile);
786
+ phases.set(phase.id, phase);
787
+ } catch (error) {
788
+ console.warn(`Failed to load phase "${entry.name}":`, error);
789
+ }
790
+ }
791
+ return { phases, entryPhaseId: null };
792
+ }
793
+ async function discoverPhaseCode(baseDir) {
794
+ const candidates = ["index.ts", "index.js"];
795
+ for (const candidate of candidates) {
796
+ const fullPath = join2(baseDir, candidate);
797
+ if (existsSync2(fullPath)) {
798
+ return fullPath;
799
+ }
800
+ }
801
+ return null;
802
+ }
803
+ async function loadPhaseCode(codePath) {
804
+ const { createJiti: createJiti2 } = await import("jiti");
805
+ const jiti = createJiti2(import.meta.url, {
806
+ moduleCache: false
807
+ });
808
+ const mod = await jiti.import(codePath, { default: true });
809
+ const fn = typeof mod === "function" ? mod : mod?.default;
810
+ if (typeof fn === "function") {
811
+ return { factory: fn };
812
+ }
813
+ if (typeof mod?.run === "function") {
814
+ return { run: mod.run };
815
+ }
816
+ throw new Error(`Phase code at "${codePath}" must export a default function or a run() function.`);
817
+ }
818
+ async function reloadPhases(registry) {
819
+ for (const [id, phase] of registry.phases) {
820
+ if (!phase.filePath) {
821
+ continue;
822
+ }
823
+ try {
824
+ const fresh = await loadPhase(phase.filePath);
825
+ registry.phases.set(id, fresh);
826
+ } catch (error) {
827
+ console.warn(`Failed to reload phase "${id}":`, error);
828
+ }
829
+ }
830
+ }
831
+
832
+ // src/harness/phases/default.ts
833
+ var DEFAULT_PHASE_ID = "default";
834
+ function createDefaultPhase() {
835
+ return {
836
+ id: DEFAULT_PHASE_ID,
837
+ name: "Execution Phase",
838
+ description: "Executes concrete task operations and produces artifacts.",
839
+ filePath: "",
840
+ baseDir: "",
841
+ content: "Execute tasks using current context.\nNo planning. No evaluation.\nRoute to next phase or stop when done."
842
+ };
843
+ }
844
+
382
845
  // src/harness/tools/index.ts
383
- import { mkdir, readFile, stat, writeFile } from "fs/promises";
384
- import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
846
+ import { mkdir, readFile as readFile3, stat, writeFile } from "fs/promises";
847
+ import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
385
848
  import Type2 from "typebox";
386
849
  import Schema from "typebox/schema";
387
850
 
851
+ // src/harness/env/path.ts
852
+ import { existsSync as existsSync3, readFileSync } from "fs";
853
+ import { homedir } from "os";
854
+ import { dirname as dirname3, isAbsolute, join as join3, parse, relative, resolve as resolve2, sep } from "path";
855
+ var WORKSPACE_ENV = "ROWAN_WORKSPACE";
856
+ var BINARY_WORKSPACE_DIR = ".rowan";
857
+ function nonEmptyEnv(env, key) {
858
+ const value = env[key]?.trim();
859
+ return value ? value : void 0;
860
+ }
861
+ function resolveUserPath(path, homeDir) {
862
+ if (path === "~") {
863
+ return homeDir;
864
+ }
865
+ if (path.startsWith("~/") || path.startsWith("~\\")) {
866
+ return join3(homeDir, path.slice(2));
867
+ }
868
+ return resolve2(path);
869
+ }
870
+ function isSourceWorkspaceRoot(path) {
871
+ const packagePath = join3(path, "package.json");
872
+ if (!existsSync3(packagePath)) {
873
+ return false;
874
+ }
875
+ try {
876
+ const manifest = JSON.parse(readFileSync(packagePath, "utf8"));
877
+ return manifest.name === "rowan-agent" || Array.isArray(manifest.workspaces);
878
+ } catch {
879
+ return false;
880
+ }
881
+ }
882
+ function findSourceWorkspaceRoot(startDir = process.cwd()) {
883
+ let current = resolve2(startDir);
884
+ const { root } = parse(current);
885
+ while (true) {
886
+ if (isSourceWorkspaceRoot(current)) {
887
+ return current;
888
+ }
889
+ if (current === root) {
890
+ return resolve2(startDir);
891
+ }
892
+ current = dirname3(current);
893
+ }
894
+ }
895
+ function defaultWorkspaceStartDir(options) {
896
+ if (options.cwd) {
897
+ return options.cwd;
898
+ }
899
+ return process.cwd();
900
+ }
901
+ function resolveWorkspaceRoot(options = {}) {
902
+ const env = options.env ?? process.env;
903
+ const homeDir = options.homeDir ?? homedir();
904
+ const override = nonEmptyEnv(env, WORKSPACE_ENV);
905
+ if (override) {
906
+ return resolveUserPath(override, homeDir);
907
+ }
908
+ return findSourceWorkspaceRoot(defaultWorkspaceStartDir(options));
909
+ }
910
+ function resolveWorkspacePaths(options = {}) {
911
+ const cwd = resolveWorkspaceRoot(options);
912
+ return {
913
+ cwd,
914
+ rowanDir: resolveProjectRowanDir(cwd, options.rowanDir)
915
+ };
916
+ }
917
+ function resolveProjectRowanDir(cwd, rowanDir = BINARY_WORKSPACE_DIR) {
918
+ const inputPath = rowanDir.trim() || BINARY_WORKSPACE_DIR;
919
+ if (isAbsolute(inputPath)) {
920
+ throw new Error(`Project Rowan dir must be a relative path: ${rowanDir}`);
921
+ }
922
+ return resolveWorkspacePath({ root: cwd }, inputPath).absolutePath;
923
+ }
924
+ function resolveInWorkspace(path, rootOrPaths) {
925
+ if (path === "~" || path.startsWith("~/") || path.startsWith("~\\")) {
926
+ return resolveUserPath(path, homedir());
927
+ }
928
+ if (isAbsolute(path)) {
929
+ return path;
930
+ }
931
+ const root = typeof rootOrPaths === "string" ? rootOrPaths : rootOrPaths.cwd;
932
+ return resolve2(root, path);
933
+ }
934
+ function normalizeRelativePath(path) {
935
+ return path.split(sep).join("/");
936
+ }
937
+ function normalizeWorkspaceInputPath(path = ".") {
938
+ const trimmed = path.trim();
939
+ if (!trimmed || trimmed === "/" || trimmed === "\\") {
940
+ return ".";
941
+ }
942
+ return path;
943
+ }
944
+ function resolveWorkspacePath(context, path = ".") {
945
+ const root = resolve2(context.root);
946
+ const inputPath = normalizeWorkspaceInputPath(path);
947
+ const absolutePath = resolve2(root, inputPath);
948
+ const relativePath = relative(root, absolutePath);
949
+ if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
950
+ throw new Error(`Path escapes workspace root: ${path}`);
951
+ }
952
+ return {
953
+ root,
954
+ inputPath,
955
+ absolutePath,
956
+ relativePath: normalizeRelativePath(relativePath || ".")
957
+ };
958
+ }
959
+
388
960
  // src/harness/tools/route-tool.ts
389
961
  import Type from "typebox";
390
962
  var PhaseRouteTool = "route";
@@ -536,18 +1108,18 @@ function normalizeCoreToolInputPath(path = ".") {
536
1108
  }
537
1109
  function createCoreToolContext(input = {}) {
538
1110
  return {
539
- root: resolve(input.root ?? process.cwd()),
1111
+ root: resolve3(input.root ?? process.cwd()),
540
1112
  maxReadBytes: input.maxReadBytes ?? DEFAULT_MAX_READ_BYTES,
541
1113
  bashTimeoutMs: input.bashTimeoutMs ?? DEFAULT_BASH_TIMEOUT_MS,
542
1114
  maxBashOutputBytes: input.maxBashOutputBytes ?? DEFAULT_MAX_BASH_OUTPUT_BYTES
543
1115
  };
544
1116
  }
545
1117
  function resolveCoreToolPath(context, path = ".") {
546
- const root = resolve(context.root);
1118
+ const root = resolve3(context.root);
547
1119
  const inputPath = normalizeCoreToolInputPath(path);
548
- const absolutePath = resolve(root, inputPath);
549
- const relativePath = relative(root, absolutePath);
550
- if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
1120
+ const absolutePath = resolve3(root, inputPath);
1121
+ const relativePath = relative2(root, absolutePath);
1122
+ if (relativePath === ".." || relativePath.startsWith(`..${sep2}`) || isAbsolute2(relativePath)) {
551
1123
  throw new Error(`Path escapes workspace root: ${path}`);
552
1124
  }
553
1125
  return {
@@ -726,7 +1298,7 @@ function createReadTool(context) {
726
1298
  error: `Not a file: ${resolved.relativePath}`
727
1299
  });
728
1300
  }
729
- const bytes = await readFile(resolved.absolutePath);
1301
+ const bytes = await readFile3(resolved.absolutePath);
730
1302
  const sliced = bytes.subarray(0, maxBytes);
731
1303
  const text = new TextDecoder().decode(sliced);
732
1304
  const resourceType = parsed.type ?? detectResourceType(resolved.absolutePath);
@@ -736,9 +1308,9 @@ function createReadTool(context) {
736
1308
  const { frontmatter } = parseFrontmatter(text);
737
1309
  const marker = resourceType === "skill" ? "SKILL.md" : "PHASE.md";
738
1310
  name = frontmatter.name ?? inferResourceName(resolved.absolutePath, marker);
739
- baseDir = dirname(resolved.absolutePath);
1311
+ baseDir = dirname4(resolved.absolutePath);
740
1312
  } else {
741
- name = inferResourceName(resolved.absolutePath, basename(resolved.absolutePath));
1313
+ name = inferResourceName(resolved.absolutePath, basename2(resolved.absolutePath));
742
1314
  }
743
1315
  return toolResult({
744
1316
  context: toolContext,
@@ -767,7 +1339,7 @@ function createWriteTool(context) {
767
1339
  async execute(args, toolContext) {
768
1340
  const parsed = WriteArgsValidator.Parse(args);
769
1341
  const resolved = resolveCoreToolPath(context, parsed.path);
770
- await mkdir(dirname(resolved.absolutePath), { recursive: true });
1342
+ await mkdir(dirname4(resolved.absolutePath), { recursive: true });
771
1343
  await writeFile(resolved.absolutePath, parsed.content, "utf8");
772
1344
  return toolResult({
773
1345
  context: toolContext,
@@ -804,7 +1376,7 @@ function createEditTool(context) {
804
1376
  });
805
1377
  }
806
1378
  const resolved = resolveCoreToolPath(context, parsed.path);
807
- const current = await readFile(resolved.absolutePath, "utf8");
1379
+ const current = await readFile3(resolved.absolutePath, "utf8");
808
1380
  if (!current.includes(parsed.oldText)) {
809
1381
  return toolResult({
810
1382
  context: toolContext,
@@ -1287,25 +1859,13 @@ async function runTurn(context, emitFn, fn) {
1287
1859
  }
1288
1860
  }
1289
1861
  async function startPhaseLoop(config, state) {
1290
- const entryPhaseId = config.phases?.entryPhaseId ?? "default";
1291
- const phases = /* @__PURE__ */ new Map();
1292
- phases.set("default", {
1293
- id: "default",
1294
- name: "Execution Phase",
1295
- description: "Executes concrete task operations and produces artifacts.",
1296
- filePath: "",
1297
- baseDir: "",
1298
- content: "Execute tasks using current context.\nNo planning. No evaluation.\nRoute to next phase or stop when done."
1299
- });
1300
- if (config.phases) {
1301
- for (const [id, phase] of config.phases.phases) {
1302
- phases.set(id, phase);
1303
- }
1862
+ const registry = config.context.phases;
1863
+ if (!registry) {
1864
+ throw new Error("AgentContext.phases is required. Construct Agent to apply the default phase.");
1865
+ }
1866
+ if (!registry.entryPhaseId) {
1867
+ throw new Error("AgentContext.phases.entryPhaseId is required. Construct Agent to apply the default phase.");
1304
1868
  }
1305
- const registry = {
1306
- phases,
1307
- entryPhaseId
1308
- };
1309
1869
  return runPhaseLoop(config, state, registry);
1310
1870
  }
1311
1871
  async function runPhaseLoop(config, state, registry) {
@@ -1316,10 +1876,6 @@ async function runPhaseLoop(config, state, registry) {
1316
1876
  let previousResults = [];
1317
1877
  let pendingInstruction = void 0;
1318
1878
  while (currentPhaseId) {
1319
- await reloadPhases(registry);
1320
- if (config.phases) {
1321
- config.phases.phases = registry.phases;
1322
- }
1323
1879
  const availablePhases = [];
1324
1880
  for (const [, phase2] of registry.phases) {
1325
1881
  availablePhases.push({ id: phase2.id, name: phase2.name, description: phase2.description, tools: phase2.tools, skills: phase2.skills, input: phase2.input, isolated: phase2.isolated });
@@ -1429,6 +1985,14 @@ async function runPhaseLoop(config, state, registry) {
1429
1985
  output = extAfter.output;
1430
1986
  }
1431
1987
  }
1988
+ const routeToolAvailable = phaseContext.tools.some((tool) => tool.name === PhaseRouteTool);
1989
+ const routeRequired = !phase.target && !phase.run && !phase.factory && routeToolAvailable && registry.phases.size > 1;
1990
+ if (routeRequired && !routeDecision) {
1991
+ throw new MissingRouteToolCallError(
1992
+ currentPhaseId,
1993
+ output.toolCalls?.map((toolCall) => toolCall.name) ?? []
1994
+ );
1995
+ }
1432
1996
  if (output.route === "continue") {
1433
1997
  isContinuing = true;
1434
1998
  continue;
@@ -1451,7 +2015,7 @@ async function runPhaseLoop(config, state, registry) {
1451
2015
  const successfulResults = await waitForBackgroundTasks(parallelTasks);
1452
2016
  previousResults = successfulResults.map((r) => ({ name: r.instanceId, output: r.payload }));
1453
2017
  pendingInstruction = routeDecision.instruction;
1454
- const entryPhaseId = phase.target ?? registry.entryPhaseId ?? "default";
2018
+ const entryPhaseId = phase.target ?? registry.entryPhaseId;
1455
2019
  if (entryPhaseId === "stop") {
1456
2020
  return completeRun(config, state, createOutcome.default(output, config.context.messages));
1457
2021
  }
@@ -1529,7 +2093,7 @@ async function withRetry(fn, options = {}) {
1529
2093
  }
1530
2094
  const delayMs = getRetryDelay(attempt, baseDelayMs, maxDelayMs);
1531
2095
  options.onRetry?.(attempt, error, delayMs);
1532
- await new Promise((resolve4) => setTimeout(resolve4, delayMs));
2096
+ await new Promise((resolve7) => setTimeout(resolve7, delayMs));
1533
2097
  if (options.signal?.aborted) {
1534
2098
  throw lastError;
1535
2099
  }
@@ -1790,12 +2354,13 @@ async function completeRun2(config, state, outcome) {
1790
2354
  }
1791
2355
 
1792
2356
  // src/harness/skills.ts
1793
- import { existsSync } from "fs";
1794
- import { readdir } from "fs/promises";
1795
- import { dirname as dirname2, join } from "path";
2357
+ import { existsSync as existsSync4, statSync as statSync2 } from "fs";
2358
+ import { readdir as readdir2 } from "fs/promises";
2359
+ import { dirname as dirname5, join as join4, resolve as resolve4 } from "path";
1796
2360
  var SKILL_MARKER = "SKILL.md";
1797
- function resolveSkillPath(input, workspace = resolveWorkspacePaths()) {
1798
- return resolveResourcePath(input, "skills", SKILL_MARKER, workspace);
2361
+ function resolveSkillPath(input) {
2362
+ const resolved = resolve4(input);
2363
+ return existsSync4(resolved) && statSync2(resolved).isDirectory() ? join4(resolved, SKILL_MARKER) : resolved;
1799
2364
  }
1800
2365
  function readSkillContent(skill) {
1801
2366
  return formatResourceOutput({
@@ -1806,35 +2371,34 @@ function readSkillContent(skill) {
1806
2371
  baseDir: skill.baseDir
1807
2372
  });
1808
2373
  }
1809
- async function loadSkill(path, workspace) {
1810
- const resolved = resolveSkillPath(path, workspace);
2374
+ async function loadSkill(path) {
2375
+ const resolved = resolveSkillPath(path);
1811
2376
  const { frontmatter, body } = await loadMarkdown(resolved);
1812
2377
  return {
1813
2378
  name: frontmatter.name ?? inferResourceName(resolved, SKILL_MARKER),
1814
2379
  description: frontmatter.description ?? "",
1815
2380
  filePath: resolved,
1816
- baseDir: dirname2(resolved),
2381
+ baseDir: dirname5(resolved),
1817
2382
  content: body,
1818
2383
  disableModelInvocation: frontmatter["disable-model-invocation"] === true
1819
2384
  };
1820
2385
  }
1821
- async function loadSkills(workspace, paths) {
1822
- if (paths && paths.length > 0) {
1823
- return Promise.all(paths.map((path) => loadSkill(path, workspace)));
2386
+ async function loadSkills(targetPath) {
2387
+ const skillsDir = resolve4(targetPath);
2388
+ if (existsSync4(skillsDir) && statSync2(skillsDir).isFile()) {
2389
+ return [await loadSkill(skillsDir)];
1824
2390
  }
1825
- const ws = workspace ?? resolveWorkspacePaths();
1826
- const skillsDir = join(ws.rowanDir, "skills");
1827
- if (!existsSync(skillsDir)) {
1828
- return [];
2391
+ if (existsSync4(join4(skillsDir, SKILL_MARKER))) {
2392
+ return [await loadSkill(skillsDir)];
1829
2393
  }
1830
- const entries = await readdir(skillsDir, { withFileTypes: true });
2394
+ const entries = await readdir2(skillsDir, { withFileTypes: true });
1831
2395
  const skills = [];
1832
- for (const entry of entries) {
2396
+ for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
1833
2397
  if (!entry.isDirectory()) continue;
1834
- const skillFile = join(skillsDir, entry.name, SKILL_MARKER);
1835
- if (!existsSync(skillFile)) continue;
2398
+ const skillFile = join4(skillsDir, entry.name, SKILL_MARKER);
2399
+ if (!existsSync4(skillFile)) continue;
1836
2400
  try {
1837
- const skill = await loadSkill(entry.name, ws);
2401
+ const skill = await loadSkill(skillFile);
1838
2402
  skills.push(skill);
1839
2403
  } catch (error) {
1840
2404
  console.warn(`Failed to load skill "${entry.name}":`, error);
@@ -1843,1850 +2407,1855 @@ async function loadSkills(workspace, paths) {
1843
2407
  return skills;
1844
2408
  }
1845
2409
 
1846
- // src/agent.ts
1847
- var Agent = class {
1848
- state;
1849
- options;
1850
- listeners = /* @__PURE__ */ new Set();
1851
- pendingListenerTasks = /* @__PURE__ */ new Set();
1852
- listenerErrors = [];
1853
- activeRun;
1854
- constructor(options) {
1855
- this.options = {
1856
- ...options,
1857
- context: cloneAgentContext(options.context)
1858
- };
1859
- this.state = {
1860
- ...this.options.sessionId ? { sessionId: this.options.sessionId } : {},
1861
- context: cloneAgentContext(this.options.context),
1862
- model: this.options.model,
1863
- tools: this.options.context.tools ?? [],
1864
- isRunning: false
1865
- };
2410
+ // src/extensions/hooks.ts
2411
+ var HookError = class extends Error {
2412
+ constructor(eventType, message, cause) {
2413
+ super(message, cause === void 0 ? void 0 : { cause });
2414
+ this.eventType = eventType;
2415
+ this.name = "HookError";
1866
2416
  }
1867
- subscribe(listener) {
1868
- this.listeners.add(listener);
1869
- return () => {
1870
- this.listeners.delete(listener);
1871
- };
1872
- }
1873
- emitToListeners(event) {
1874
- for (const listener of this.listeners) {
1875
- try {
1876
- const result = listener(event);
1877
- if (result && typeof result === "object" && "then" in result) {
1878
- const task = Promise.resolve(result).catch((error) => {
1879
- this.listenerErrors.push(error);
1880
- }).finally(() => {
1881
- this.pendingListenerTasks.delete(task);
1882
- });
1883
- this.pendingListenerTasks.add(task);
1884
- }
1885
- } catch (error) {
1886
- this.listenerErrors.push(error);
1887
- }
1888
- }
1889
- }
1890
- async flushEvents() {
1891
- while (this.pendingListenerTasks.size > 0) {
1892
- await Promise.all([...this.pendingListenerTasks]);
1893
- }
1894
- for (const listener of this.listeners) {
1895
- try {
1896
- await listener.flush?.();
1897
- } catch (error) {
1898
- this.listenerErrors.push(error);
1899
- }
1900
- }
1901
- if (this.listenerErrors.length > 0) {
1902
- const [error] = this.listenerErrors;
1903
- throw error instanceof Error ? error : new Error(String(error));
1904
- }
1905
- }
1906
- processEvents(event) {
1907
- switch (event.type) {
1908
- case "message_start":
1909
- this.state.currentResult = void 0;
1910
- break;
1911
- case "message_end":
1912
- break;
1913
- case "agent_end":
1914
- break;
1915
- }
1916
- this.emitToListeners(event);
1917
- this.options.extensionRunnerRef?.current?.emitAgentEvent(event);
2417
+ eventType;
2418
+ };
2419
+ var HooksManager = class {
2420
+ handlers = /* @__PURE__ */ new Map();
2421
+ /**
2422
+ * Register a hook handler.
2423
+ * Handlers execute in registration order.
2424
+ *
2425
+ * @param eventType - Event type
2426
+ * @param handler - Handler function
2427
+ */
2428
+ on(eventType, handler) {
2429
+ const handlers = this.handlers.get(eventType) ?? [];
2430
+ handlers.push(handler);
2431
+ this.handlers.set(eventType, handlers);
1918
2432
  }
1919
2433
  /**
1920
- * Hook for before_tool_call — called before a tool executes.
1921
- * Extensions can block execution by setting allow=false.
2434
+ * Unregister a hook handler.
1922
2435
  */
1923
- async handleBeforeToolCall(tool, args) {
1924
- const runner = this.options.extensionRunnerRef?.current;
1925
- if (!runner) return { allow: true };
1926
- return runner.emitBeforeToolCall(tool, args);
2436
+ off(eventType, handler) {
2437
+ const handlers = this.handlers.get(eventType);
2438
+ if (!handlers) return;
2439
+ const index = handlers.indexOf(handler);
2440
+ if (index >= 0) handlers.splice(index, 1);
1927
2441
  }
1928
2442
  /**
1929
- * Hook for after_tool_call — called after a tool executes.
1930
- * Extensions can mutate the result.
2443
+ * Clear all handlers, or clear handlers for specified event type.
1931
2444
  */
1932
- async handleAfterToolCall(tool, result) {
1933
- const runner = this.options.extensionRunnerRef?.current;
1934
- if (!runner) return result;
1935
- return runner.emitAfterToolCall(tool, result);
2445
+ clear(eventType) {
2446
+ if (eventType) {
2447
+ this.handlers.delete(eventType);
2448
+ } else {
2449
+ this.handlers.clear();
2450
+ }
1936
2451
  }
1937
2452
  /**
1938
- * Hook for before_phase — called before a phase executes.
1939
- * Extensions can abort, skip, or replace the phase input.
2453
+ * Check if there are handlers registered for specified event type.
1940
2454
  */
1941
- async handleBeforePhase(phaseId, input) {
1942
- const runner = this.options.extensionRunnerRef?.current;
1943
- if (!runner) return {};
1944
- return runner.emitBeforePhase(phaseId, input);
2455
+ has(eventType) {
2456
+ const handlers = this.handlers.get(eventType);
2457
+ return handlers !== void 0 && handlers.length > 0;
1945
2458
  }
1946
2459
  /**
1947
- * Hook for after_phase — called after a phase executes.
1948
- * Extensions can abort, retry, or replace the output.
2460
+ * Get handler count for specified event type.
1949
2461
  */
1950
- async handleAfterPhase(phaseId, output) {
1951
- const runner = this.options.extensionRunnerRef?.current;
1952
- if (!runner) return {};
1953
- return runner.emitAfterPhase(phaseId, output);
2462
+ count(eventType) {
2463
+ return this.handlers.get(eventType)?.length ?? 0;
1954
2464
  }
1955
2465
  /**
1956
- * Hook for before_prompt — called before model request, allowing extensions to transform PhaseContext.
1957
- * Extensions can transform the PhaseContext (messages, tools, systemPrompt, etc.).
2466
+ * Emit event (listen-only, ignore return values).
2467
+ * Errors are collected and thrown.
2468
+ *
2469
+ * @param eventType - Event type
2470
+ * @param event - Event object
1958
2471
  */
1959
- async handleBeforePrompt(phaseId, input) {
1960
- const runner = this.options.extensionRunnerRef?.current;
1961
- if (!runner) return input;
1962
- return runner.emitBeforePrompt(phaseId, input);
1963
- }
1964
- handleRunFailure(error, aborted) {
1965
- const message = error instanceof Error ? error.message : "Agent run failed.";
1966
- this.state.error = message;
2472
+ async emit(eventType, event) {
2473
+ const handlers = this.handlers.get(eventType);
2474
+ if (!handlers?.length) return;
2475
+ const errors = [];
2476
+ await Promise.allSettled(
2477
+ handlers.map(async (handler) => {
2478
+ try {
2479
+ await handler(event);
2480
+ } catch (error) {
2481
+ errors.push(
2482
+ error instanceof Error ? error : new Error(String(error))
2483
+ );
2484
+ }
2485
+ })
2486
+ );
2487
+ if (errors.length > 0) {
2488
+ throw new HookError(
2489
+ eventType,
2490
+ `${errors.length} handler(s) failed for "${eventType}"`,
2491
+ errors[0]
2492
+ );
2493
+ }
1967
2494
  }
1968
2495
  /**
1969
- * Discover and load phases and skills from the workspace.
2496
+ * Emit event and return first non-undefined result (short-circuit).
1970
2497
  *
1971
- * - Auto-discovers file-based phases from .rowan/phases/
1972
- * - Auto-discovers skills from .rowan/skills/
1973
- * - Merges with CLI-provided phases and skills
2498
+ * @param eventType - Event type
2499
+ * @param event - Event object
2500
+ * @returns First non-undefined result, or undefined
1974
2501
  */
1975
- async discoverResources(context) {
1976
- const workspace = resolveWorkspacePaths({ cwd: this.options.cwd, rowanDir: this.options.rowanDir });
1977
- const filePhases = await loadPhases(workspace);
1978
- let phases;
1979
- const providedPhases = this.options.phases;
1980
- if (providedPhases && providedPhases.phases.size > 0) {
1981
- const merged = new Map(filePhases.phases);
1982
- for (const [id, phase] of providedPhases.phases) {
1983
- merged.set(id, phase);
2502
+ async emitFirst(eventType, event) {
2503
+ const handlers = this.handlers.get(eventType);
2504
+ if (!handlers?.length) return void 0;
2505
+ for (const handler of handlers) {
2506
+ try {
2507
+ const result = await handler(event);
2508
+ if (result !== void 0) return result;
2509
+ } catch (error) {
2510
+ throw new HookError(
2511
+ eventType,
2512
+ `Handler failed for "${eventType}"`,
2513
+ error instanceof Error ? error : new Error(String(error))
2514
+ );
1984
2515
  }
1985
- phases = {
1986
- phases: merged,
1987
- entryPhaseId: providedPhases.entryPhaseId ?? filePhases.entryPhaseId
1988
- };
1989
- } else if (filePhases.phases.size > 0) {
1990
- phases = filePhases;
1991
- }
1992
- const fileSkills = await loadSkills(workspace);
1993
- const skillMap = /* @__PURE__ */ new Map();
1994
- for (const skill of fileSkills) {
1995
- skillMap.set(skill.name, skill);
1996
- }
1997
- for (const skill of context.skills) {
1998
- skillMap.set(skill.name, skill);
1999
2516
  }
2000
- const skills = Array.from(skillMap.values());
2001
- return { phases, skills };
2517
+ return void 0;
2002
2518
  }
2003
- async runWithLifecycle(executor) {
2004
- if (this.activeRun) {
2005
- throw new Error("Agent is already running.");
2006
- }
2007
- let resolvePromise;
2008
- const abortController = new AbortController();
2009
- const promise = new Promise((resolve4) => {
2010
- resolvePromise = resolve4;
2011
- });
2012
- this.activeRun = { promise, resolve: resolvePromise, abortController };
2013
- this.state.isRunning = true;
2014
- try {
2015
- const result = await executor(abortController.signal);
2016
- resolvePromise(result);
2017
- return result;
2018
- } catch (error) {
2019
- this.handleRunFailure(error, abortController.signal.aborted);
2020
- resolvePromise(void 0);
2021
- throw error;
2022
- } finally {
2023
- this.finishRun();
2519
+ };
2520
+ var _globalHooks;
2521
+ function getGlobalHooks() {
2522
+ _globalHooks ??= new HooksManager();
2523
+ return _globalHooks;
2524
+ }
2525
+ function resetGlobalHooks() {
2526
+ _globalHooks = void 0;
2527
+ }
2528
+
2529
+ // src/extensions/runner.ts
2530
+ import { execFile } from "child_process";
2531
+ import {
2532
+ registerModel as registerModel2,
2533
+ unregisterProviderModels,
2534
+ registerApiProvider
2535
+ } from "@rowan-agent/models";
2536
+
2537
+ // src/extensions/types.ts
2538
+ function createSourceInfo(extensionPath, options = {}) {
2539
+ const source = options.source ?? (extensionPath.startsWith("<") ? "synthetic" : "local");
2540
+ const displayName = extensionPath.startsWith("<") ? extensionPath.slice(1, -1) : extensionPath.split("/").pop() ?? extensionPath;
2541
+ return {
2542
+ source,
2543
+ baseDir: options.baseDir,
2544
+ displayName
2545
+ };
2546
+ }
2547
+ function createExtension(extensionPath, sourceInfo) {
2548
+ return {
2549
+ path: extensionPath,
2550
+ sourceInfo,
2551
+ handlers: /* @__PURE__ */ new Map(),
2552
+ tools: /* @__PURE__ */ new Map(),
2553
+ phases: /* @__PURE__ */ new Map()
2554
+ };
2555
+ }
2556
+ function createExtensionRuntime() {
2557
+ const state = {};
2558
+ const assertActive = () => {
2559
+ if (state.staleMessage) {
2560
+ throw new Error(state.staleMessage);
2024
2561
  }
2025
- }
2026
- finishRun() {
2027
- this.state.isRunning = false;
2028
- this.activeRun = void 0;
2029
- }
2030
- async run(config) {
2031
- const resolved = this.resolveRunConfig(config);
2032
- const previousSessionId = this.state.sessionId ?? this.options.sessionId;
2033
- const sessionId = resolved.sessionId ?? this.state.sessionId;
2034
- const hadExistingSession = Boolean(sessionId && previousSessionId === sessionId);
2035
- this.options = resolved;
2036
- if (sessionId) {
2037
- this.state.sessionId = sessionId;
2562
+ };
2563
+ const runtime = {
2564
+ assertActive,
2565
+ invalidate: (message) => {
2566
+ state.staleMessage ??= message ?? "This extension context is stale after session replacement or reload. Do not use a captured extension API after the runner has been replaced.";
2567
+ },
2568
+ pendingProviderRegistrations: [],
2569
+ // Pre-bind: queue registrations so bind() can flush them once the
2570
+ // model registry is available. bind() replaces both with direct calls.
2571
+ registerProvider: (name, config, extensionPath = "<unknown>") => {
2572
+ runtime.pendingProviderRegistrations.push({ name, config, extensionPath });
2573
+ },
2574
+ unregisterProvider: (name) => {
2575
+ runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter(
2576
+ (r) => r.name !== name
2577
+ );
2038
2578
  }
2039
- this.state.context = cloneAgentContext(resolved.context);
2040
- this.state.model = resolved.model;
2041
- this.state.tools = resolved.context.tools ?? [];
2042
- this.state.currentResult = void 0;
2043
- this.state.error = void 0;
2044
- const { phases, skills } = await this.discoverResources(resolved.context);
2045
- const contextWithSkills = {
2046
- ...resolved.context,
2047
- skills
2048
- };
2049
- return this.runWithLifecycle(async (signal) => {
2050
- const emit = (event) => {
2051
- this.processEvents(event);
2579
+ };
2580
+ return runtime;
2581
+ }
2582
+
2583
+ // src/extensions/event-bus.ts
2584
+ function createEventBus() {
2585
+ const listeners = /* @__PURE__ */ new Map();
2586
+ return {
2587
+ on(event, listener) {
2588
+ const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
2589
+ set.add(listener);
2590
+ listeners.set(event, set);
2591
+ return () => {
2592
+ set.delete(listener);
2593
+ if (set.size === 0) listeners.delete(event);
2052
2594
  };
2053
- const beforeToolCall = async (input) => {
2054
- if (resolved.beforeToolCall) {
2055
- const userResult = await resolved.beforeToolCall(input);
2056
- if (!userResult.allow) return userResult;
2595
+ },
2596
+ emit(event, ...args) {
2597
+ const set = listeners.get(event);
2598
+ if (!set) return;
2599
+ for (const listener of set) {
2600
+ try {
2601
+ listener(...args);
2602
+ } catch (err) {
2603
+ console.error(`[event-bus] Listener error for "${event}":`, err);
2057
2604
  }
2058
- const extResult = await this.handleBeforeToolCall(input.tool, input.args);
2059
- if (!extResult.allow) {
2060
- return { allow: false, reason: extResult.reason ?? "Blocked by extension" };
2061
- }
2062
- return { allow: true };
2063
- };
2064
- const afterToolCall = async (input) => {
2065
- let result2 = input.result;
2066
- if (resolved.afterToolCall) {
2067
- result2 = await resolved.afterToolCall({ tool: input.tool, result: result2 });
2605
+ }
2606
+ },
2607
+ off(event) {
2608
+ if (event) {
2609
+ listeners.delete(event);
2610
+ } else {
2611
+ listeners.clear();
2612
+ }
2613
+ },
2614
+ has(event) {
2615
+ const set = listeners.get(event);
2616
+ return set !== void 0 && set.size > 0;
2617
+ },
2618
+ count(event) {
2619
+ return listeners.get(event)?.size ?? 0;
2620
+ }
2621
+ };
2622
+ }
2623
+
2624
+ // src/extensions/runner.ts
2625
+ async function execCommand(command, args, cwd, options) {
2626
+ return new Promise((resolve7, reject) => {
2627
+ const child = execFile(
2628
+ command,
2629
+ args,
2630
+ {
2631
+ cwd: options?.cwd ?? cwd,
2632
+ env: options?.env ? { ...process.env, ...options.env } : void 0,
2633
+ timeout: options?.timeout,
2634
+ maxBuffer: 10 * 1024 * 1024
2635
+ },
2636
+ (error, stdout, stderr) => {
2637
+ if (error && error.killed && options?.signal?.aborted) {
2638
+ reject(new Error("Command was aborted"));
2639
+ return;
2068
2640
  }
2069
- return this.handleAfterToolCall(input.tool, result2);
2070
- };
2071
- const result = await runAgentLoop({
2072
- context: contextWithSkills,
2073
- sessionId: sessionId ?? createId("ses"),
2074
- model: resolved.model,
2075
- stream: resolved.stream,
2076
- maxAttempts: resolved.maxAttempts,
2077
- signal,
2078
- beforeToolCall,
2079
- afterToolCall,
2080
- beforePhase: (phaseId, input) => this.handleBeforePhase(phaseId, input),
2081
- afterPhase: (phaseId, output) => this.handleAfterPhase(phaseId, output),
2082
- beforePrompt: (phaseId, input) => this.handleBeforePrompt(phaseId, input),
2083
- phases,
2084
- emit,
2085
- onMessage: resolved.onMessage,
2086
- onOutcome: resolved.onOutcome,
2087
- onModelTranscript: resolved.onModelTranscript
2088
- });
2089
- this.state.sessionId = result.sessionId;
2090
- this.state.context = {
2091
- ...cloneAgentContext(resolved.context),
2092
- messages: snapshotMessages(result.messages)
2093
- };
2094
- this.state.currentResult = result;
2095
- this.options = {
2096
- ...resolved,
2097
- sessionId: result.sessionId,
2098
- context: cloneAgentContext(this.state.context)
2099
- };
2100
- return result;
2641
+ resolve7({
2642
+ exitCode: typeof error?.code === "number" ? error.code : error ? 1 : 0,
2643
+ stdout: stdout ?? "",
2644
+ stderr: stderr ?? ""
2645
+ });
2646
+ }
2647
+ );
2648
+ if (options?.signal) {
2649
+ options.signal.addEventListener(
2650
+ "abort",
2651
+ () => {
2652
+ child.kill("SIGTERM");
2653
+ },
2654
+ { once: true }
2655
+ );
2656
+ }
2657
+ });
2658
+ }
2659
+ function applyProviderRegistration(config) {
2660
+ if (config.streamSimple) {
2661
+ registerApiProvider({ protocol: config.protocol, stream: config.streamSimple });
2662
+ }
2663
+ for (const modelConfig of config.models) {
2664
+ registerModel2({
2665
+ id: modelConfig.id,
2666
+ name: modelConfig.name,
2667
+ protocol: config.protocol,
2668
+ provider: config.id,
2669
+ baseUrl: config.baseUrl,
2670
+ reasoning: modelConfig.reasoning,
2671
+ input: modelConfig.input,
2672
+ cost: modelConfig.cost,
2673
+ contextWindow: modelConfig.contextWindow,
2674
+ maxTokens: modelConfig.maxTokens,
2675
+ ...config.headers ? { headers: config.headers } : {}
2101
2676
  });
2102
2677
  }
2103
- abort(reason = "Aborted by caller.") {
2104
- this.activeRun?.abortController.abort(reason);
2678
+ }
2679
+ function applyProviderUnregistration(name) {
2680
+ unregisterProviderModels(name);
2681
+ }
2682
+ var ExtensionRunner = class {
2683
+ hooks;
2684
+ runtime;
2685
+ events;
2686
+ cwd;
2687
+ abortController = new AbortController();
2688
+ _idle = true;
2689
+ // Per-extension tracking
2690
+ extensions = [];
2691
+ // Phase management
2692
+ phases = /* @__PURE__ */ new Map();
2693
+ _phaseCache = null;
2694
+ // Provider management
2695
+ pendingProviders = [];
2696
+ bound = false;
2697
+ // Error listeners
2698
+ errorListeners = /* @__PURE__ */ new Set();
2699
+ // Loaded extension metadata (pre-initialization form)
2700
+ loadedExtensions = [];
2701
+ /** Current agent context — set by the agent before each phase */
2702
+ currentContext;
2703
+ constructor(options) {
2704
+ this.hooks = new HooksManager();
2705
+ this.runtime = createExtensionRuntime();
2706
+ this.events = createEventBus();
2707
+ this.cwd = options?.cwd ?? process.cwd();
2708
+ }
2709
+ /** Whether the agent is currently idle (not streaming). */
2710
+ get isIdle() {
2711
+ return this._idle;
2712
+ }
2713
+ /** Set idle state — called by the agent loop. */
2714
+ setIdle(idle) {
2715
+ this._idle = idle;
2716
+ }
2717
+ /** Abort signal for the current runner instance. */
2718
+ get signal() {
2719
+ return this.abortController.signal;
2720
+ }
2721
+ /** Abort the current runner operation. */
2722
+ abort() {
2723
+ this.abortController.abort();
2105
2724
  }
2725
+ // ---------------------------------------------------------------------------
2726
+ // Error handling
2727
+ // ---------------------------------------------------------------------------
2106
2728
  /**
2107
- * Get formatted skill content for LLM consumption.
2108
- *
2109
- * Finds the skill by name and returns the formatted content using `formatSkillInvocation`.
2110
- * This is a programmatic API for developers to invoke skills directly.
2111
- *
2112
- * @param name - The skill name to look up
2113
- * @param additionalInstructions - Optional additional instructions to append
2114
- * @returns Formatted skill content string
2729
+ * Register an error listener.
2730
+ * Returns an unsubscribe function.
2115
2731
  */
2116
- skill(name, additionalInstructions) {
2117
- const skill = this.state.context.skills.find((s) => s.name === name);
2118
- if (!skill) {
2119
- throw new Error(`Unknown skill: ${name}`);
2732
+ onError(listener) {
2733
+ this.errorListeners.add(listener);
2734
+ return () => this.errorListeners.delete(listener);
2735
+ }
2736
+ /**
2737
+ * Emit a structured extension error to all listeners.
2738
+ */
2739
+ emitError(error) {
2740
+ for (const listener of this.errorListeners) {
2741
+ try {
2742
+ listener(error);
2743
+ } catch (err) {
2744
+ console.error("[extension-runner] Error listener failed:", err);
2745
+ }
2120
2746
  }
2121
- const content = readSkillContent(skill);
2122
- return additionalInstructions ? `${content}
2123
-
2124
- ${additionalInstructions}` : content;
2125
2747
  }
2748
+ // ---------------------------------------------------------------------------
2749
+ // Lifecycle: invalidate / assertActive
2750
+ // ---------------------------------------------------------------------------
2126
2751
  /**
2127
- * Get formatted phase content for LLM consumption.
2128
- *
2129
- * Finds the phase by name and returns the formatted content.
2130
- * This is a programmatic API for developers to invoke phases directly.
2752
+ * Mark all extension contexts as stale.
2753
+ * After calling this, any captured ExtensionAPI or ExtensionContext will throw
2754
+ * on use. Used during session replacement or reload.
2755
+ */
2756
+ invalidate(message) {
2757
+ this.runtime.invalidate(message);
2758
+ }
2759
+ // ---------------------------------------------------------------------------
2760
+ // Direct hook API
2761
+ // ---------------------------------------------------------------------------
2762
+ /**
2763
+ * Subscribe to a specific hook event type.
2764
+ * Returns an unsubscribe function.
2131
2765
  *
2132
- * @param name - The phase name to look up
2133
- * @returns Formatted phase content string, or empty string if not found
2766
+ * @example
2767
+ * ```ts
2768
+ * const unsub = runner.on("before_tool_call", (event) => {
2769
+ * return { allow: false, reason: "Blocked" };
2770
+ * });
2771
+ * unsub(); // Cancel subscription
2772
+ * ```
2134
2773
  */
2135
- async phase(name) {
2136
- const workspace = resolveWorkspacePaths({ cwd: this.options.cwd, rowanDir: this.options.rowanDir });
2137
- const { loadPhase: loadPhase2, readPhaseContent: readPhaseContent2 } = await import("./loader-HCYCHPGT.js");
2138
- try {
2139
- const phase = await loadPhase2(name, workspace);
2140
- return readPhaseContent2(phase);
2141
- } catch {
2142
- return "";
2143
- }
2774
+ on(type, handler) {
2775
+ this.hooks.on(type, handler);
2776
+ return () => this.hooks.off(type, handler);
2144
2777
  }
2145
- async waitForIdle() {
2146
- if (!this.activeRun) {
2147
- return;
2778
+ /**
2779
+ * Subscribe to all events (read-only).
2780
+ * Returns an unsubscribe function.
2781
+ *
2782
+ * @example
2783
+ * ```ts
2784
+ * const unsub = runner.subscribe((event) => {
2785
+ * console.log(event.type);
2786
+ * });
2787
+ * ```
2788
+ */
2789
+ subscribe(listener) {
2790
+ const handlers = /* @__PURE__ */ new Map();
2791
+ for (const eventType of this.getAllEventTypes()) {
2792
+ const handler = (event) => listener(event);
2793
+ handlers.set(eventType, handler);
2794
+ this.hooks.on(eventType, handler);
2148
2795
  }
2149
- await this.activeRun.promise.catch(() => void 0);
2150
- await this.flushEvents().catch(() => void 0);
2151
- }
2152
- resolveRunConfig(config) {
2153
- const context = cloneAgentContext(config?.context ?? this.createContextSnapshot());
2154
- return {
2155
- ...this.options,
2156
- ...config,
2157
- context,
2158
- sessionId: config?.sessionId ?? this.state.sessionId ?? this.options.sessionId
2796
+ return () => {
2797
+ for (const [eventType, handler] of handlers) {
2798
+ this.hooks.off(eventType, handler);
2799
+ }
2159
2800
  };
2160
2801
  }
2161
- createContextSnapshot() {
2162
- return cloneAgentContext(this.state.context);
2163
- }
2164
- };
2165
- function cloneAgentContext(context) {
2166
- return {
2167
- systemPrompt: context.systemPrompt,
2168
- messages: snapshotMessages(context.messages),
2169
- tools: context.tools.slice(),
2170
- skills: context.skills.slice()
2171
- };
2172
- }
2173
-
2174
- // src/harness/session/session.ts
2175
- import Type3 from "typebox";
2176
- var SESSION_SCHEMA_VERSION = "0.4.4";
2177
- var AgentMessageSchema = Type3.Object({
2178
- id: Type3.String(),
2179
- role: Type3.Union([
2180
- Type3.Literal("system"),
2181
- Type3.Literal("user"),
2182
- Type3.Literal("assistant"),
2183
- Type3.Literal("tool")
2184
- ]),
2185
- content: Type3.Union([Type3.String(), Type3.Array(Type3.Unknown())]),
2186
- createdAt: Type3.String(),
2187
- metadata: Type3.Optional(Type3.Record(Type3.String(), Type3.Unknown()))
2188
- });
2189
- var SkillSchema = Type3.Object({
2190
- name: Type3.String(),
2191
- description: Type3.String(),
2192
- filePath: Type3.String(),
2193
- baseDir: Type3.String(),
2194
- content: Type3.String(),
2195
- disableModelInvocation: Type3.Boolean()
2196
- });
2197
- function createMessage2(role, content, metadata) {
2198
- return {
2199
- id: createId("msg"),
2200
- role,
2201
- content,
2202
- createdAt: createTimestamp(),
2203
- ...metadata ? { metadata } : {}
2204
- };
2205
- }
2206
- function createSession(input) {
2207
- const createdAt = createTimestamp();
2208
- const messages = [
2209
- createMessage2("user", input.input)
2210
- ];
2211
- return {
2212
- version: SESSION_SCHEMA_VERSION,
2213
- id: input.id ?? createId("ses"),
2214
- ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
2215
- systemPrompt: input.systemPrompt,
2216
- input: input.input,
2217
- messages,
2218
- log: [],
2219
- skills: input.skills ?? [],
2220
- createdAt,
2221
- updatedAt: createdAt,
2222
- ...input.title ? { title: input.title } : {}
2223
- };
2224
- }
2225
- function appendUserTurn(session, input) {
2226
- session.messages.push(createMessage2("user", input));
2227
- session.updatedAt = createTimestamp();
2228
- return session;
2229
- }
2230
-
2231
- // src/harness/session/session-manager.ts
2232
- var SESSION_MANAGER_SCHEMA_VERSION = "0.4.4";
2233
- function clone(value) {
2234
- return JSON.parse(JSON.stringify(value));
2235
- }
2236
- function createSessionHeader(input) {
2237
- const createdAt = createTimestamp();
2238
- return {
2239
- type: "header",
2240
- id: input.id ?? createId("ses"),
2241
- version: SESSION_MANAGER_SCHEMA_VERSION,
2242
- createdAt,
2243
- updatedAt: createdAt,
2244
- systemPrompt: input.systemPrompt,
2245
- input: input.input,
2246
- ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
2247
- skills: input.skills?.map(clone) ?? [],
2248
- ...input.title ? { title: input.title } : {},
2249
- currentLeafId: null
2250
- };
2251
- }
2252
- function filterExecutionTurns(steps, filter = {}) {
2253
- return steps.filter((step) => {
2254
- if (filter.phase && step.phase !== filter.phase) return false;
2255
- if (filter.afterMs !== void 0 && step.requestedAtMs < filter.afterMs) return false;
2256
- return true;
2257
- }).map(clone);
2258
- }
2259
- var InMemorySessionManager = class _InMemorySessionManager {
2260
- constructor(header, entries = []) {
2261
- this.header = header;
2262
- this.entries = entries;
2263
- }
2264
- header;
2265
- entries;
2266
- static create(input) {
2267
- return new _InMemorySessionManager(createSessionHeader(input));
2802
+ getAllEventTypes() {
2803
+ return [
2804
+ "before_phase",
2805
+ "after_phase",
2806
+ "before_prompt",
2807
+ "before_tool_call",
2808
+ "after_tool_call",
2809
+ "agent_start",
2810
+ "agent_end",
2811
+ "turn_start",
2812
+ "turn_end",
2813
+ "message_start",
2814
+ "message_update",
2815
+ "message_end",
2816
+ "tool_execution_start",
2817
+ "tool_execution_update",
2818
+ "tool_execution_end",
2819
+ "queue_update",
2820
+ "save_point",
2821
+ "abort",
2822
+ "settled"
2823
+ ];
2268
2824
  }
2269
- static fromRecords(records) {
2270
- const [header, ...entries] = records;
2271
- if (!header || header.type !== "header") {
2272
- throw new Error("Session records must start with a header.");
2825
+ // ---------------------------------------------------------------------------
2826
+ // Extension loading
2827
+ // ---------------------------------------------------------------------------
2828
+ /**
2829
+ * Load and initialize extensions.
2830
+ * Creates Extension tracking objects and calls each factory with an ExtensionAPI.
2831
+ */
2832
+ async loadExtensions(extensions) {
2833
+ for (const ext of extensions) {
2834
+ try {
2835
+ const sourceInfo = createSourceInfo(ext.path, {
2836
+ source: "local",
2837
+ baseDir: ext.path.startsWith("<") ? void 0 : ext.path
2838
+ });
2839
+ const extension = createExtension(ext.path, sourceInfo);
2840
+ const api = this.createExtensionAPI(extension, ext.manifest);
2841
+ await ext.factory(api);
2842
+ this.extensions.push(extension);
2843
+ this.loadedExtensions.push(ext);
2844
+ this._phaseCache = null;
2845
+ } catch (error) {
2846
+ const message = error instanceof Error ? error.message : String(error);
2847
+ this.emitError({
2848
+ extensionPath: ext.path,
2849
+ event: "load",
2850
+ error: message,
2851
+ stack: error instanceof Error ? error.stack : void 0
2852
+ });
2853
+ throw error;
2854
+ }
2273
2855
  }
2274
- return new _InMemorySessionManager(clone(header), clone(entries));
2275
2856
  }
2276
- getSessionId() {
2277
- return this.header.id;
2857
+ // ---------------------------------------------------------------------------
2858
+ // Tool management
2859
+ // ---------------------------------------------------------------------------
2860
+ /**
2861
+ * Get all registered tools from all extensions (first registration per name wins).
2862
+ */
2863
+ getAllRegisteredTools() {
2864
+ const toolsByName = /* @__PURE__ */ new Map();
2865
+ for (const ext of this.extensions) {
2866
+ for (const tool of ext.tools.values()) {
2867
+ if (!toolsByName.has(tool.definition.name)) {
2868
+ toolsByName.set(tool.definition.name, tool);
2869
+ }
2870
+ }
2871
+ }
2872
+ return Array.from(toolsByName.values());
2278
2873
  }
2279
- getSessionFile() {
2874
+ /**
2875
+ * Get a tool definition by name. Returns undefined if not found.
2876
+ */
2877
+ getToolDefinition(toolName) {
2878
+ for (const ext of this.extensions) {
2879
+ const tool = ext.tools.get(toolName);
2880
+ if (tool) return tool.definition;
2881
+ }
2280
2882
  return void 0;
2281
2883
  }
2282
- async getHeader() {
2283
- return clone(this.header);
2284
- }
2285
- async appendMessage(message) {
2286
- return this.appendEntry({ type: "message", message: clone(message) });
2287
- }
2288
- async appendOutcome(outcome) {
2289
- return this.appendEntry({ type: "outcome", outcome: clone(outcome) });
2290
- }
2291
- async appendExecutionTurn(turn) {
2292
- return this.appendEntry({
2293
- type: "execution_turn",
2294
- turn: clone({
2295
- ...turn,
2296
- sessionId: this.header.id
2297
- })
2298
- });
2299
- }
2300
- async appendCompaction(input) {
2301
- return this.appendEntry({ type: "compaction", ...input });
2302
- }
2303
- async appendBranchSummary(input) {
2304
- return this.appendEntry({ type: "branch_summary", ...input });
2305
- }
2306
- async appendSessionInfo(input) {
2307
- this.header = {
2308
- ...this.header,
2309
- title: input.title,
2310
- updatedAt: createTimestamp()
2311
- };
2312
- return this.appendEntry({ type: "session_info", title: input.title });
2313
- }
2314
- async appendCustom(input) {
2315
- return this.appendEntry({ type: "custom", customType: input.customType, data: clone(input.data) });
2884
+ // ---------------------------------------------------------------------------
2885
+ // Handler queries
2886
+ // ---------------------------------------------------------------------------
2887
+ /**
2888
+ * Check if there are handlers registered for specified event type.
2889
+ */
2890
+ hasHandlers(eventType) {
2891
+ return this.hooks.has(eventType);
2316
2892
  }
2317
- async appendSessionState(state) {
2318
- return this.appendEntry({ type: "session_state", state: clone(state) });
2893
+ /**
2894
+ * Get the number of handlers for specified event type.
2895
+ */
2896
+ handlerCount(eventType) {
2897
+ return this.hooks.count(eventType);
2319
2898
  }
2320
- async appendModelTranscript(transcript, meta) {
2321
- return this.appendEntry({
2322
- type: "model_transcript",
2323
- transcript: clone(transcript),
2324
- ...meta?.phase ? { phase: meta.phase } : {},
2325
- ...meta?.model ? { model: clone(meta.model) } : {}
2326
- });
2899
+ // ---------------------------------------------------------------------------
2900
+ // Phase management
2901
+ // ---------------------------------------------------------------------------
2902
+ getPhase(id) {
2903
+ const reg = this.getRegisteredPhase(id);
2904
+ if (!reg) return void 0;
2905
+ return this.adaptToPhase(reg);
2327
2906
  }
2328
- async getSessionState() {
2329
- for (let i = this.entries.length - 1; i >= 0; i--) {
2330
- const entry = this.entries[i];
2331
- if (entry.type === "session_state") {
2332
- return clone(entry.state);
2333
- }
2334
- }
2335
- return void 0;
2907
+ getPhases() {
2908
+ return [...this.collectRegisteredPhases().values()].map(
2909
+ (p) => this.adaptToPhase(p)
2910
+ );
2336
2911
  }
2337
- async branch(entryId) {
2338
- if (entryId !== null && !this.entries.some((entry) => entry.id === entryId)) {
2339
- throw new Error(`Session entry not found: ${entryId}`);
2912
+ createPhaseRegistry(input = {}) {
2913
+ const registered = this.collectRegisteredPhases();
2914
+ const phases = /* @__PURE__ */ new Map();
2915
+ for (const [id, reg] of registered) {
2916
+ phases.set(id, this.adaptToPhase(reg));
2340
2917
  }
2341
- this.header = {
2342
- ...this.header,
2343
- currentLeafId: entryId,
2344
- updatedAt: createTimestamp()
2345
- };
2918
+ const entryPhaseId = input.entryPhaseId ?? null;
2919
+ return { phases, entryPhaseId };
2346
2920
  }
2347
- async buildAgentContext(input = {}) {
2348
- const entries = this.entriesForLeaf(input.leafId ?? this.header.currentLeafId ?? null);
2921
+ /** Adapt an extension RegisteredPhase to the core Phase type. */
2922
+ adaptToPhase(reg) {
2923
+ const def = reg.definition;
2349
2924
  return {
2350
- systemPrompt: this.header.systemPrompt,
2351
- messages: entries.filter((entry) => entry.type === "message").map((entry) => entry.message).map(clone),
2352
- tools: input.tools?.slice() ?? [],
2353
- skills: input.skills?.map(clone) ?? this.header.skills.map(clone)
2354
- };
2355
- }
2356
- async listEntries() {
2357
- return this.entries.map(clone);
2358
- }
2359
- async loadExecutionTurns(filter) {
2360
- return filterExecutionTurns(
2361
- this.entries.filter((entry) => entry.type === "execution_turn").map((entry) => entry.turn),
2362
- filter
2363
- );
2364
- }
2365
- appendImportedEntry(entry) {
2366
- this.entries.push(clone(entry));
2367
- this.header = {
2368
- ...this.header,
2369
- currentLeafId: entry.id,
2370
- updatedAt: entry.timestamp
2925
+ id: def.id,
2926
+ name: def.name ?? def.id,
2927
+ description: def.description ?? "",
2928
+ tools: def.tools,
2929
+ skills: def.skills,
2930
+ target: def.target,
2931
+ input: def.input,
2932
+ run: def.run,
2933
+ filePath: "",
2934
+ baseDir: "",
2935
+ content: "",
2936
+ ...def.model ? { model: parseModelRef(def.model) } : {}
2371
2937
  };
2372
2938
  }
2373
- appendEntry(input) {
2374
- const timestamp = createTimestamp();
2375
- const entry = {
2376
- id: createId("entry"),
2377
- parentId: this.header.currentLeafId ?? null,
2378
- timestamp,
2379
- ...input
2939
+ // ---------------------------------------------------------------------------
2940
+ // Lifecycle
2941
+ // ---------------------------------------------------------------------------
2942
+ /**
2943
+ * Bind the runner — flushes pending provider registrations and
2944
+ * replaces runtime stubs with real implementations.
2945
+ */
2946
+ bind() {
2947
+ if (this.bound) return;
2948
+ this.bound = true;
2949
+ this.flushPendingProviders();
2950
+ this.runtime.registerProvider = (_name, config) => {
2951
+ applyProviderRegistration(config);
2380
2952
  };
2381
- this.entries.push(entry);
2382
- this.header = {
2383
- ...this.header,
2384
- currentLeafId: entry.id,
2385
- updatedAt: timestamp
2953
+ this.runtime.unregisterProvider = (_name) => {
2954
+ applyProviderUnregistration(_name);
2386
2955
  };
2387
- return entry.id;
2388
- }
2389
- entriesForLeaf(leafId) {
2390
- if (!leafId) {
2391
- return [];
2392
- }
2393
- const byId = new Map(this.entries.map((entry) => [entry.id, entry]));
2394
- const ordered = [];
2395
- let currentId = leafId;
2396
- while (currentId) {
2397
- const entry = byId.get(currentId);
2398
- if (!entry) {
2399
- throw new Error(`Session entry not found: ${currentId}`);
2400
- }
2401
- ordered.unshift(entry);
2402
- currentId = entry.parentId;
2403
- }
2404
- return ordered;
2405
- }
2406
- };
2407
- function summarizeSessionManagerRecords(records) {
2408
- const [header, ...entries] = records;
2409
- if (!header || header.type !== "header") {
2410
- throw new Error("Session records must start with a header.");
2411
- }
2412
- const messages = entries.filter((entry) => entry.type === "message").map((entry) => entry.message);
2413
- const latestMessageContent = messages.at(-1)?.content;
2414
- const latestMessage = latestMessageContent ? messageContentText(latestMessageContent) : void 0;
2415
- return {
2416
- id: header.id,
2417
- ...header.title ? { title: header.title } : {},
2418
- createdAt: header.createdAt,
2419
- updatedAt: header.updatedAt,
2420
- messageCount: messages.length,
2421
- ...latestMessage ? { latestMessage } : {}
2422
- };
2423
- }
2424
-
2425
- // src/harness/session/jsonl.ts
2426
- import { appendFile, mkdir as mkdir2, readFile as readFile2, readdir as readdir2, stat as stat2, unlink, writeFile as writeFile2 } from "fs/promises";
2427
- import { join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
2428
- var SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_-]+$/;
2429
- function isPathInside(parent, child) {
2430
- const relativePath = relative2(parent, child);
2431
- return Boolean(relativePath) && !relativePath.startsWith("..") && !relativePath.includes(`..${sep2}`);
2432
- }
2433
- function safeSessionPath(sessionsDir, id) {
2434
- if (!SESSION_ID_PATTERN.test(id)) {
2435
- return void 0;
2436
- }
2437
- const root = resolve2(sessionsDir);
2438
- const path = resolve2(root, `${id}.jsonl`);
2439
- return isPathInside(root, path) ? path : void 0;
2440
- }
2441
- function parseRecord(line) {
2442
- const value = JSON.parse(line);
2443
- if (!value || typeof value !== "object" || !("type" in value)) {
2444
- throw new Error("Invalid session JSONL record.");
2445
2956
  }
2446
- return value;
2447
- }
2448
- async function readRecords(path) {
2449
- const text = await readFile2(path, "utf8").catch((error) => {
2450
- if (error.code === "ENOENT") {
2451
- return void 0;
2452
- }
2453
- throw error;
2454
- });
2455
- if (!text) {
2456
- return void 0;
2957
+ // ---------------------------------------------------------------------------
2958
+ // Unified hook emission
2959
+ // ---------------------------------------------------------------------------
2960
+ /**
2961
+ * Generic emit — fire-and-forget for any event type.
2962
+ */
2963
+ async emit(eventType, event) {
2964
+ await this.hooks.emit(eventType, event);
2457
2965
  }
2458
- const records = text.split("\n").map((line) => line.trim()).filter(Boolean).map(parseRecord);
2459
- const header = records[0];
2460
- if (!header || header.type !== "header") {
2461
- throw new Error("Session JSONL must start with a header record.");
2966
+ /**
2967
+ * Unified hook emission — returns the first non-undefined result.
2968
+ */
2969
+ async emitHook(type, event) {
2970
+ return this.hooks.emitFirst(type, event);
2462
2971
  }
2463
- const lastEntry = records.slice(1).reverse().find((record) => record.type !== "header");
2464
- return [
2465
- {
2466
- ...header,
2467
- updatedAt: lastEntry?.timestamp ?? header.updatedAt,
2468
- currentLeafId: lastEntry?.id ?? header.currentLeafId ?? null
2469
- },
2470
- ...records.slice(1)
2471
- ];
2472
- }
2473
- async function appendRecord(path, record) {
2474
- await appendFile(path, `${JSON.stringify(record)}
2475
- `, "utf8");
2476
- }
2477
- var LocalJsonlSessionManager = class _LocalJsonlSessionManager {
2478
- constructor(sessionsDir, filePath, inner) {
2479
- this.sessionsDir = sessionsDir;
2480
- this.filePath = filePath;
2481
- this.inner = inner;
2972
+ // ---------------------------------------------------------------------------
2973
+ // Phase hooks (with inline processing)
2974
+ // ---------------------------------------------------------------------------
2975
+ async emitBeforePhase(phaseId, input) {
2976
+ const result = await this.emitHook("before_phase", {
2977
+ type: "before_phase",
2978
+ phaseId,
2979
+ input
2980
+ });
2981
+ return result ?? {};
2482
2982
  }
2483
- sessionsDir;
2484
- filePath;
2485
- inner;
2486
- static async create(sessionsDir, input) {
2487
- const header = createSessionHeader(input);
2488
- const path = safeSessionPath(sessionsDir, header.id);
2489
- if (!path) {
2490
- throw new Error(`Invalid session id: ${header.id}`);
2491
- }
2492
- const exists = await stat2(path).then(() => true).catch((error) => {
2493
- if (error.code === "ENOENT") {
2494
- return false;
2495
- }
2496
- throw error;
2983
+ async emitAfterPhase(phaseId, output) {
2984
+ const result = await this.emitHook("after_phase", {
2985
+ type: "after_phase",
2986
+ phaseId,
2987
+ output
2497
2988
  });
2498
- if (exists) {
2499
- throw new Error(`Session already exists: ${header.id}`);
2500
- }
2501
- await mkdir2(sessionsDir, { recursive: true });
2502
- await writeFile2(path, `${JSON.stringify(header)}
2503
- `, "utf8");
2504
- return new _LocalJsonlSessionManager(
2505
- sessionsDir,
2506
- path,
2507
- InMemorySessionManager.fromRecords([header])
2508
- );
2989
+ return result ?? {};
2509
2990
  }
2510
- static async open(sessionsDir, id) {
2511
- const path = safeSessionPath(sessionsDir, id);
2512
- if (!path) {
2513
- return void 0;
2514
- }
2515
- const records = await readRecords(path);
2516
- if (!records) {
2517
- return void 0;
2518
- }
2519
- const header = records[0];
2520
- if (header.id !== id) {
2521
- throw new Error(`Session id mismatch: expected ${id}, found ${header.id}`);
2522
- }
2523
- return new _LocalJsonlSessionManager(
2524
- sessionsDir,
2525
- path,
2526
- InMemorySessionManager.fromRecords(records)
2527
- );
2991
+ async emitBeforePrompt(phaseId, input) {
2992
+ const result = await this.emitHook("before_prompt", {
2993
+ type: "before_prompt",
2994
+ phaseId,
2995
+ input
2996
+ });
2997
+ return result?.input ?? input;
2528
2998
  }
2529
- static async list(sessionsDir) {
2530
- const entries = await readdir2(sessionsDir, { withFileTypes: true }).catch((error) => {
2531
- if (error.code === "ENOENT") {
2532
- return [];
2533
- }
2534
- throw error;
2999
+ async emitBeforeToolCall(tool, args) {
3000
+ const result = await this.emitHook("before_tool_call", {
3001
+ type: "before_tool_call",
3002
+ tool,
3003
+ args
2535
3004
  });
2536
- const sessions = await Promise.all(
2537
- entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map(async (entry) => {
2538
- const path = join2(sessionsDir, entry.name);
2539
- const records = await readRecords(path);
2540
- return records ? summarizeSessionManagerRecords(records) : void 0;
2541
- })
2542
- );
2543
- return sessions.filter((session) => Boolean(session)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
3005
+ return result ?? { allow: true };
2544
3006
  }
2545
- static async delete(sessionsDir, id) {
2546
- const path = safeSessionPath(sessionsDir, id);
2547
- if (!path) {
2548
- return false;
2549
- }
2550
- return unlink(path).then(() => true).catch((error) => {
2551
- if (error.code === "ENOENT") {
2552
- return false;
2553
- }
2554
- throw error;
3007
+ async emitAfterToolCall(tool, result) {
3008
+ const hookResult = await this.emitHook("after_tool_call", {
3009
+ type: "after_tool_call",
3010
+ tool,
3011
+ result
2555
3012
  });
3013
+ return hookResult?.result ?? result;
2556
3014
  }
2557
- getSessionId() {
2558
- return this.inner.getSessionId();
3015
+ // ---------------------------------------------------------------------------
3016
+ // Agent event hooks (fire-and-forget)
3017
+ // ---------------------------------------------------------------------------
3018
+ async emitAgentStart(sessionId) {
3019
+ await this.hooks.emit("agent_start", { type: "agent_start", sessionId });
2559
3020
  }
2560
- getSessionFile() {
2561
- return this.filePath;
3021
+ async emitAgentEnd(sessionId, outcome, messages) {
3022
+ await this.hooks.emit("agent_end", { type: "agent_end", sessionId, outcome, messages });
2562
3023
  }
2563
- async getHeader() {
2564
- return this.inner.getHeader();
3024
+ async emitTurnStart(messages) {
3025
+ await this.hooks.emit("turn_start", { type: "turn_start", messages });
2565
3026
  }
2566
- async appendMessage(message) {
2567
- return this.appendThroughInner(() => this.inner.appendMessage(message));
3027
+ async emitTurnEnd(messages, outcome) {
3028
+ await this.hooks.emit("turn_end", { type: "turn_end", messages, outcome });
2568
3029
  }
2569
- async appendOutcome(outcome) {
2570
- return this.appendThroughInner(() => this.inner.appendOutcome(outcome));
3030
+ async emitMessageStart(message) {
3031
+ await this.hooks.emit("message_start", { type: "message_start", message });
2571
3032
  }
2572
- async appendExecutionTurn(turn) {
2573
- return this.appendThroughInner(() => this.inner.appendExecutionTurn(turn));
3033
+ async emitMessageUpdate(message, delta) {
3034
+ await this.hooks.emit("message_update", { type: "message_update", message, delta });
2574
3035
  }
2575
- async appendCompaction(input) {
2576
- return this.appendThroughInner(() => this.inner.appendCompaction(input));
3036
+ async emitMessageEnd(message) {
3037
+ await this.hooks.emit("message_end", { type: "message_end", message });
2577
3038
  }
2578
- async appendBranchSummary(input) {
2579
- return this.appendThroughInner(() => this.inner.appendBranchSummary(input));
3039
+ async emitToolExecutionStart(toolCallId, toolName, args) {
3040
+ await this.hooks.emit("tool_execution_start", {
3041
+ type: "tool_execution_start",
3042
+ toolCallId,
3043
+ toolName,
3044
+ args
3045
+ });
2580
3046
  }
2581
- async appendSessionInfo(input) {
2582
- return this.appendThroughInner(() => this.inner.appendSessionInfo(input));
3047
+ async emitToolExecutionUpdate(toolCallId, toolName, _progress) {
3048
+ await this.hooks.emit("tool_execution_update", {
3049
+ type: "tool_execution_update",
3050
+ toolCallId,
3051
+ toolName
3052
+ });
2583
3053
  }
2584
- async appendCustom(input) {
2585
- return this.appendThroughInner(() => this.inner.appendCustom(input));
3054
+ async emitToolExecutionEnd(toolCallId, toolName, result) {
3055
+ await this.hooks.emit("tool_execution_end", {
3056
+ type: "tool_execution_end",
3057
+ toolCallId,
3058
+ toolName,
3059
+ result
3060
+ });
2586
3061
  }
2587
- async appendSessionState(state) {
2588
- return this.appendThroughInner(() => this.inner.appendSessionState(state));
3062
+ async emitSavePoint(hadPendingMutations) {
3063
+ await this.hooks.emit("save_point", { type: "save_point", hadPendingMutations });
2589
3064
  }
2590
- async appendModelTranscript(transcript, meta) {
2591
- return this.appendThroughInner(() => this.inner.appendModelTranscript(transcript, meta));
3065
+ async emitAbort(reason) {
3066
+ await this.hooks.emit("abort", { type: "abort", reason });
2592
3067
  }
2593
- async getSessionState() {
2594
- return this.inner.getSessionState();
3068
+ async emitSettled() {
3069
+ await this.hooks.emit("settled", { type: "settled" });
2595
3070
  }
2596
- async branch(entryId) {
2597
- await this.inner.branch(entryId);
2598
- if (entryId) {
2599
- await this.appendThroughInner(() => this.inner.appendBranchSummary({
2600
- fromId: entryId,
2601
- summary: `Selected branch at ${entryId}`
2602
- }));
3071
+ // ---------------------------------------------------------------------------
3072
+ // AgentEvent bridge (backward compatibility)
3073
+ // ---------------------------------------------------------------------------
3074
+ /**
3075
+ * Emit an AgentEvent by routing to the appropriate typed hook.
3076
+ */
3077
+ async emitAgentEvent(event) {
3078
+ switch (event.type) {
3079
+ case "agent_start":
3080
+ await this.emitAgentStart(event.sessionId);
3081
+ break;
3082
+ case "agent_end":
3083
+ await this.emitAgentEnd(event.sessionId, event.outcome, event.messages);
3084
+ break;
3085
+ case "turn_start":
3086
+ await this.emitTurnStart(event.messages);
3087
+ break;
3088
+ case "turn_end":
3089
+ await this.emitTurnEnd(event.messages, event.outcome);
3090
+ break;
3091
+ case "message_start":
3092
+ await this.emitMessageStart(event.message);
3093
+ break;
3094
+ case "message_update":
3095
+ await this.emitMessageUpdate(event.message, event.delta);
3096
+ break;
3097
+ case "message_end":
3098
+ await this.emitMessageEnd(event.message);
3099
+ break;
3100
+ case "tool_execution_start":
3101
+ await this.emitToolExecutionStart(event.toolCallId, event.toolName, event.args);
3102
+ break;
3103
+ case "tool_execution_update":
3104
+ await this.emitToolExecutionUpdate(event.toolCallId, event.toolName);
3105
+ break;
3106
+ case "tool_execution_end":
3107
+ await this.emitToolExecutionEnd(event.toolCallId, event.toolName, event.result);
3108
+ break;
2603
3109
  }
2604
3110
  }
2605
- async buildAgentContext(input) {
2606
- return this.inner.buildAgentContext(input);
2607
- }
2608
- async listEntries() {
2609
- return this.inner.listEntries();
2610
- }
2611
- async loadExecutionTurns(filter) {
2612
- return this.inner.loadExecutionTurns(filter);
3111
+ // ---------------------------------------------------------------------------
3112
+ // Internal helpers
3113
+ // ---------------------------------------------------------------------------
3114
+ /**
3115
+ * Create an ExtensionAPI for a specific extension.
3116
+ * Registration methods write to the extension tracking object.
3117
+ * Action methods delegate to the shared runtime.
3118
+ */
3119
+ createExtensionAPI(extension, manifest) {
3120
+ const runner = this;
3121
+ const extContext = {
3122
+ get cwd() {
3123
+ return runner.cwd;
3124
+ },
3125
+ get signal() {
3126
+ return runner.abortController.signal;
3127
+ },
3128
+ isIdle() {
3129
+ return runner._idle;
3130
+ },
3131
+ abort() {
3132
+ runner.abortController.abort();
3133
+ },
3134
+ exec(command, args, options) {
3135
+ return execCommand(command, args, runner.cwd, options);
3136
+ },
3137
+ manifest,
3138
+ getSystemPrompt() {
3139
+ return runner.currentContext?.systemPrompt ?? "";
3140
+ },
3141
+ setSystemPrompt(prompt) {
3142
+ if (runner.currentContext) runner.currentContext.systemPrompt = prompt;
3143
+ },
3144
+ getMessages() {
3145
+ return runner.currentContext?.messages ?? [];
3146
+ },
3147
+ addMessage(role, content) {
3148
+ runner.currentContext?.messages.push({ role, content });
3149
+ },
3150
+ getAvailableTools() {
3151
+ return (runner.currentContext?.tools ?? []).map((t) => ({ name: t.name, description: t.description }));
3152
+ },
3153
+ getAvailableSkills() {
3154
+ return (runner.currentContext?.skills ?? []).map((s) => ({ name: s.name, description: s.description }));
3155
+ },
3156
+ getSkillContent(skillName) {
3157
+ const skill = runner.currentContext?.skills.find((s) => s.name === skillName);
3158
+ return skill?.content ?? "";
3159
+ },
3160
+ getAvailablePhases() {
3161
+ return [...runner.phases.keys()];
3162
+ },
3163
+ getPhaseContent(phaseId) {
3164
+ return runner.phases.get(phaseId)?.definition.description ?? "";
3165
+ }
3166
+ };
3167
+ return createExtensionAPI(this.hooks, extension.path, {
3168
+ registerPhase: (registration) => this.registerPhase(extension, registration),
3169
+ registerProvider: (config) => this.registerProvider(config),
3170
+ unregisterProvider: (name) => this.unregisterProvider(name),
3171
+ registerTool: (tool) => this.registerTool(extension, tool),
3172
+ context: extContext,
3173
+ manifest
3174
+ }, this.runtime, this.events);
2613
3175
  }
2614
- async appendThroughInner(append) {
2615
- const before = await this.inner.listEntries();
2616
- const entryId = await append();
2617
- const after = await this.inner.listEntries();
2618
- const entry = after.find((candidate) => candidate.id === entryId);
2619
- if (!entry || before.some((candidate) => candidate.id === entry.id)) {
2620
- throw new Error(`Session entry was not appended: ${entryId}`);
3176
+ registerTool(extension, tool) {
3177
+ for (const ext of this.extensions) {
3178
+ if (ext.tools.has(tool.name)) {
3179
+ this.emitError({
3180
+ extensionPath: extension.path,
3181
+ event: "register_tool",
3182
+ error: `Tool "${tool.name}" is already registered by extension ${ext.path}`
3183
+ });
3184
+ return;
3185
+ }
2621
3186
  }
2622
- await mkdir2(this.sessionsDir, { recursive: true });
2623
- await appendRecord(this.filePath, entry);
2624
- return entryId;
2625
- }
2626
- };
2627
-
2628
- // src/event-stream.ts
2629
- var EventStream = class {
2630
- queue = [];
2631
- waiting = [];
2632
- done = false;
2633
- finalResultPromise;
2634
- resolveFinalResult;
2635
- isComplete;
2636
- extractResult;
2637
- constructor(isComplete, extractResult) {
2638
- this.isComplete = isComplete;
2639
- this.extractResult = extractResult;
2640
- this.finalResultPromise = new Promise((resolve4) => {
2641
- this.resolveFinalResult = resolve4;
3187
+ const sourceInfo = createSourceInfo(extension.path);
3188
+ extension.tools.set(tool.name, {
3189
+ definition: tool,
3190
+ sourceInfo
2642
3191
  });
2643
3192
  }
2644
- push(event) {
2645
- if (this.done) return;
2646
- if (this.isComplete(event)) {
2647
- this.done = true;
2648
- this.resolveFinalResult(this.extractResult(event));
3193
+ registerPhase(extension, registration) {
3194
+ if (!registration.id) {
3195
+ throw new Error(`Phase registration requires an "id" field.`);
2649
3196
  }
2650
- const waiter = this.waiting.shift();
2651
- if (waiter) {
2652
- waiter({ value: event, done: false });
2653
- } else {
2654
- this.queue.push(event);
3197
+ if (!registration.name) {
3198
+ throw new Error(`Phase registration "${registration.id}" requires a "name" field.`);
3199
+ }
3200
+ if (!registration.description) {
3201
+ throw new Error(`Phase registration "${registration.id}" requires a "description" field.`);
2655
3202
  }
3203
+ if (this.phases.has(registration.id)) {
3204
+ throw new Error(`Duplicate phase id: ${registration.id}`);
3205
+ }
3206
+ const definition = {
3207
+ id: registration.id,
3208
+ name: registration.name,
3209
+ description: registration.description,
3210
+ run: registration.run,
3211
+ ...registration.model ? { model: registration.model } : {}
3212
+ };
3213
+ const registered = {
3214
+ definition,
3215
+ source: { extensionPath: extension.path }
3216
+ };
3217
+ this.phases.set(registration.id, registered);
3218
+ extension.phases.set(registration.id, registered);
3219
+ this._phaseCache = null;
2656
3220
  }
2657
- end(result) {
2658
- this.done = true;
2659
- if (result !== void 0) {
2660
- this.resolveFinalResult(result);
3221
+ registerProvider(config) {
3222
+ if (this.bound) {
3223
+ applyProviderRegistration(config);
3224
+ } else {
3225
+ this.pendingProviders.push({ kind: "register", config });
2661
3226
  }
2662
- while (this.waiting.length > 0) {
2663
- const waiter = this.waiting.shift();
2664
- waiter({ value: void 0, done: true });
3227
+ }
3228
+ unregisterProvider(name) {
3229
+ if (this.bound) {
3230
+ applyProviderUnregistration(name);
3231
+ } else {
3232
+ this.pendingProviders.push({ kind: "unregister", name });
2665
3233
  }
2666
3234
  }
2667
- async *[Symbol.asyncIterator]() {
2668
- while (true) {
2669
- if (this.queue.length > 0) {
2670
- yield this.queue.shift();
2671
- } else if (this.done) {
2672
- return;
3235
+ flushPendingProviders() {
3236
+ for (const action of this.pendingProviders) {
3237
+ if (action.kind === "register") {
3238
+ applyProviderRegistration(action.config);
2673
3239
  } else {
2674
- const result = await new Promise((resolve4) => this.waiting.push(resolve4));
2675
- if (result.done) return;
2676
- yield result.value;
3240
+ applyProviderUnregistration(action.name);
2677
3241
  }
2678
3242
  }
3243
+ this.pendingProviders.length = 0;
2679
3244
  }
2680
- result() {
2681
- return this.finalResultPromise;
3245
+ getRegisteredPhase(id) {
3246
+ return this.collectRegisteredPhases().get(id);
2682
3247
  }
2683
- };
2684
- var AgentEventStream = class extends EventStream {
2685
- constructor() {
2686
- super(
2687
- (event) => event.type === "agent_end",
2688
- (event) => event.type === "agent_end" ? event.messages : []
2689
- );
3248
+ collectRegisteredPhases() {
3249
+ if (this._phaseCache) return this._phaseCache;
3250
+ this._phaseCache = new Map(this.phases);
3251
+ return this._phaseCache;
2690
3252
  }
2691
3253
  };
3254
+ function createExtensionRunner(options) {
3255
+ return new ExtensionRunner(options);
3256
+ }
2692
3257
 
2693
- // src/extensions/hooks.ts
2694
- var HookError = class extends Error {
2695
- constructor(eventType, message, cause) {
2696
- super(message, cause === void 0 ? void 0 : { cause });
2697
- this.eventType = eventType;
2698
- this.name = "HookError";
2699
- }
2700
- eventType;
2701
- };
2702
- var HooksManager = class {
2703
- handlers = /* @__PURE__ */ new Map();
2704
- /**
2705
- * Register a hook handler.
2706
- * Handlers execute in registration order.
2707
- *
2708
- * @param eventType - Event type
2709
- * @param handler - Handler function
2710
- */
2711
- on(eventType, handler) {
2712
- const handlers = this.handlers.get(eventType) ?? [];
2713
- handlers.push(handler);
2714
- this.handlers.set(eventType, handlers);
2715
- }
2716
- /**
2717
- * Unregister a hook handler.
2718
- */
2719
- off(eventType, handler) {
2720
- const handlers = this.handlers.get(eventType);
2721
- if (!handlers) return;
2722
- const index = handlers.indexOf(handler);
2723
- if (index >= 0) handlers.splice(index, 1);
2724
- }
2725
- /**
2726
- * Clear all handlers, or clear handlers for specified event type.
2727
- */
2728
- clear(eventType) {
2729
- if (eventType) {
2730
- this.handlers.delete(eventType);
2731
- } else {
2732
- this.handlers.clear();
2733
- }
2734
- }
2735
- /**
2736
- * Check if there are handlers registered for specified event type.
2737
- */
2738
- has(eventType) {
2739
- const handlers = this.handlers.get(eventType);
2740
- return handlers !== void 0 && handlers.length > 0;
2741
- }
2742
- /**
2743
- * Get handler count for specified event type.
2744
- */
2745
- count(eventType) {
2746
- return this.handlers.get(eventType)?.length ?? 0;
2747
- }
2748
- /**
2749
- * Emit event (listen-only, ignore return values).
2750
- * Errors are collected and thrown.
2751
- *
2752
- * @param eventType - Event type
2753
- * @param event - Event object
2754
- */
2755
- async emit(eventType, event) {
2756
- const handlers = this.handlers.get(eventType);
2757
- if (!handlers?.length) return;
2758
- const errors = [];
2759
- await Promise.allSettled(
2760
- handlers.map(async (handler) => {
2761
- try {
2762
- await handler(event);
2763
- } catch (error) {
2764
- errors.push(
2765
- error instanceof Error ? error : new Error(String(error))
2766
- );
2767
- }
2768
- })
2769
- );
2770
- if (errors.length > 0) {
2771
- throw new HookError(
2772
- eventType,
2773
- `${errors.length} handler(s) failed for "${eventType}"`,
2774
- errors[0]
2775
- );
2776
- }
2777
- }
2778
- /**
2779
- * Emit event and return first non-undefined result (short-circuit).
2780
- *
2781
- * @param eventType - Event type
2782
- * @param event - Event object
2783
- * @returns First non-undefined result, or undefined
2784
- */
2785
- async emitFirst(eventType, event) {
2786
- const handlers = this.handlers.get(eventType);
2787
- if (!handlers?.length) return void 0;
2788
- for (const handler of handlers) {
2789
- try {
2790
- const result = await handler(event);
2791
- if (result !== void 0) return result;
2792
- } catch (error) {
2793
- throw new HookError(
2794
- eventType,
2795
- `Handler failed for "${eventType}"`,
2796
- error instanceof Error ? error : new Error(String(error))
2797
- );
2798
- }
2799
- }
3258
+ // src/extensions/loader.ts
3259
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
3260
+ import { readdir as readdir3, readFile as readFile4, stat as stat2 } from "fs/promises";
3261
+ import { fileURLToPath } from "url";
3262
+ import { dirname as dirname6, extname as extname2, join as join5, resolve as resolve5 } from "path";
3263
+ import { createJiti } from "jiti";
3264
+ function isExtensionFile(path) {
3265
+ const extension = extname2(path).toLowerCase();
3266
+ return extension === ".ts" || extension === ".js";
3267
+ }
3268
+ function isSyntheticPath(path) {
3269
+ return path.startsWith("<") && path.endsWith(">");
3270
+ }
3271
+ function readManifestSync(dir) {
3272
+ const manifestPath = join5(dir, "package.json");
3273
+ if (!existsSync5(manifestPath)) {
3274
+ return void 0;
3275
+ }
3276
+ try {
3277
+ const content = readFileSync2(manifestPath, "utf8");
3278
+ const pkg = JSON.parse(content);
3279
+ const rowan = pkg.rowan;
3280
+ if (!rowan) return void 0;
3281
+ return {
3282
+ entry: rowan.extensions?.[0],
3283
+ phase: rowan.phase
3284
+ };
3285
+ } catch {
2800
3286
  return void 0;
2801
3287
  }
2802
- };
2803
- var _globalHooks;
2804
- function getGlobalHooks() {
2805
- _globalHooks ??= new HooksManager();
2806
- return _globalHooks;
2807
3288
  }
2808
- function resetGlobalHooks() {
2809
- _globalHooks = void 0;
3289
+ function jitiAliases() {
3290
+ const moduleUrl = import.meta.url;
3291
+ if (!moduleUrl) {
3292
+ return {};
3293
+ }
3294
+ const agentSourcePath = fileURLToPath(new URL("../index.ts", moduleUrl));
3295
+ if (!existsSync5(agentSourcePath)) {
3296
+ return {};
3297
+ }
3298
+ return { "@rowan-agent/agent": agentSourcePath };
2810
3299
  }
2811
-
2812
- // src/extensions/runner.ts
2813
- import { execFile } from "child_process";
2814
- import {
2815
- registerModel,
2816
- unregisterProviderModels,
2817
- registerApiProvider
2818
- } from "@rowan-agent/models";
2819
-
2820
- // src/extensions/types.ts
2821
- function createSourceInfo(extensionPath, options = {}) {
2822
- const source = options.source ?? (extensionPath.startsWith("<") ? "synthetic" : "local");
2823
- const displayName = extensionPath.startsWith("<") ? extensionPath.slice(1, -1) : extensionPath.split("/").pop() ?? extensionPath;
2824
- return {
2825
- source,
2826
- baseDir: options.baseDir,
2827
- displayName
2828
- };
3300
+ var sharedJiti;
3301
+ function getJiti() {
3302
+ sharedJiti ??= createJiti(import.meta.url || process.cwd(), {
3303
+ moduleCache: false,
3304
+ alias: jitiAliases()
3305
+ });
3306
+ return sharedJiti;
2829
3307
  }
2830
- function createExtension(extensionPath, resolvedPath, sourceInfo) {
2831
- return {
2832
- path: extensionPath,
2833
- resolvedPath,
2834
- sourceInfo,
2835
- handlers: /* @__PURE__ */ new Map(),
2836
- tools: /* @__PURE__ */ new Map(),
2837
- phases: /* @__PURE__ */ new Map()
2838
- };
3308
+ async function loadExtensionModule(extensionPath) {
3309
+ const jiti = getJiti();
3310
+ const module = await jiti.import(extensionPath, { default: true });
3311
+ return typeof module === "function" ? module : void 0;
2839
3312
  }
2840
- function createExtensionRuntime() {
2841
- const state = {};
2842
- const assertActive = () => {
2843
- if (state.staleMessage) {
2844
- throw new Error(state.staleMessage);
2845
- }
2846
- };
2847
- const runtime = {
2848
- assertActive,
2849
- invalidate: (message) => {
2850
- state.staleMessage ??= message ?? "This extension context is stale after session replacement or reload. Do not use a captured extension API after the runner has been replaced.";
2851
- },
2852
- pendingProviderRegistrations: [],
2853
- // Pre-bind: queue registrations so bind() can flush them once the
2854
- // model registry is available. bind() replaces both with direct calls.
2855
- registerProvider: (name, config, extensionPath = "<unknown>") => {
2856
- runtime.pendingProviderRegistrations.push({ name, config, extensionPath });
2857
- },
2858
- unregisterProvider: (name) => {
2859
- runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter(
2860
- (r) => r.name !== name
2861
- );
2862
- }
2863
- };
2864
- return runtime;
3313
+ async function readPackageManifest(path) {
3314
+ if (!existsSync5(path)) {
3315
+ return void 0;
3316
+ }
3317
+ const content = await readFile4(path, "utf8");
3318
+ return JSON.parse(content);
2865
3319
  }
2866
-
2867
- // src/extensions/event-bus.ts
2868
- function createEventBus() {
2869
- const listeners = /* @__PURE__ */ new Map();
2870
- return {
2871
- on(event, listener) {
2872
- const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
2873
- set.add(listener);
2874
- listeners.set(event, set);
2875
- return () => {
2876
- set.delete(listener);
2877
- if (set.size === 0) listeners.delete(event);
2878
- };
2879
- },
2880
- emit(event, ...args) {
2881
- const set = listeners.get(event);
2882
- if (!set) return;
2883
- for (const listener of set) {
2884
- try {
2885
- listener(...args);
2886
- } catch (err) {
2887
- console.error(`[event-bus] Listener error for "${event}":`, err);
2888
- }
2889
- }
2890
- },
2891
- off(event) {
2892
- if (event) {
2893
- listeners.delete(event);
2894
- } else {
2895
- listeners.clear();
2896
- }
2897
- },
2898
- has(event) {
2899
- const set = listeners.get(event);
2900
- return set !== void 0 && set.size > 0;
2901
- },
2902
- count(event) {
2903
- return listeners.get(event)?.size ?? 0;
3320
+ async function resolveExtensionEntries(dir) {
3321
+ const manifest = await readPackageManifest(join5(dir, "package.json"));
3322
+ const declared = manifest?.rowan?.extensions;
3323
+ if (declared && declared.length > 0) {
3324
+ return declared.map((entry) => resolve5(dir, entry));
3325
+ }
3326
+ for (const name of ["index.ts", "index.js"]) {
3327
+ const entry = join5(dir, name);
3328
+ if (existsSync5(entry)) {
3329
+ return [entry];
2904
3330
  }
2905
- };
3331
+ }
3332
+ return void 0;
2906
3333
  }
2907
-
2908
- // src/extensions/runner.ts
2909
- async function execCommand(command, args, cwd, options) {
2910
- return new Promise((resolve4, reject) => {
2911
- const child = execFile(
2912
- command,
2913
- args,
2914
- {
2915
- cwd: options?.cwd ?? cwd,
2916
- env: options?.env ? { ...process.env, ...options.env } : void 0,
2917
- timeout: options?.timeout,
2918
- maxBuffer: 10 * 1024 * 1024
2919
- },
2920
- (error, stdout, stderr) => {
2921
- if (error && error.killed && options?.signal?.aborted) {
2922
- reject(new Error("Command was aborted"));
2923
- return;
3334
+ async function discoverExtensionsInDir(dir) {
3335
+ const entries = await readdir3(dir, { withFileTypes: true }).catch((error) => {
3336
+ if (error.code === "ENOENT") {
3337
+ return [];
3338
+ }
3339
+ throw error;
3340
+ });
3341
+ const paths = [];
3342
+ for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
3343
+ const entryPath = join5(dir, entry.name);
3344
+ if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
3345
+ paths.push(entryPath);
3346
+ continue;
3347
+ }
3348
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
3349
+ const info = entry.isSymbolicLink() ? await stat2(entryPath).catch(() => void 0) : void 0;
3350
+ if (entry.isDirectory() || info?.isDirectory()) {
3351
+ const resolvedEntries = await resolveExtensionEntries(entryPath);
3352
+ if (resolvedEntries) {
3353
+ paths.push(...resolvedEntries);
2924
3354
  }
2925
- resolve4({
2926
- exitCode: typeof error?.code === "number" ? error.code : error ? 1 : 0,
2927
- stdout: stdout ?? "",
2928
- stderr: stderr ?? ""
2929
- });
2930
3355
  }
2931
- );
2932
- if (options?.signal) {
2933
- options.signal.addEventListener(
2934
- "abort",
2935
- () => {
2936
- child.kill("SIGTERM");
2937
- },
2938
- { once: true }
2939
- );
2940
3356
  }
2941
- });
2942
- }
2943
- function applyProviderRegistration(config) {
2944
- if (config.streamSimple) {
2945
- registerApiProvider({ protocol: config.protocol, stream: config.streamSimple });
2946
- }
2947
- for (const modelConfig of config.models) {
2948
- registerModel({
2949
- id: modelConfig.id,
2950
- name: modelConfig.name,
2951
- protocol: config.protocol,
2952
- provider: config.id,
2953
- baseUrl: config.baseUrl,
2954
- reasoning: modelConfig.reasoning,
2955
- input: modelConfig.input,
2956
- cost: modelConfig.cost,
2957
- contextWindow: modelConfig.contextWindow,
2958
- maxTokens: modelConfig.maxTokens,
2959
- ...config.headers ? { headers: config.headers } : {}
2960
- });
2961
3357
  }
3358
+ return paths;
2962
3359
  }
2963
- function applyProviderUnregistration(name) {
2964
- unregisterProviderModels(name);
3360
+ function loadExtensionFromFactory(factory, cwd, extensionPath = "<inline>") {
3361
+ const resolvedCwd = resolve5(cwd);
3362
+ const resolvedPath = isSyntheticPath(extensionPath) ? extensionPath : resolve5(resolvedCwd, extensionPath);
3363
+ const name = isSyntheticPath(extensionPath) ? extensionPath : dirname6(resolvedPath).split("/").pop() ?? "unknown";
3364
+ const manifestDir = isSyntheticPath(extensionPath) ? resolvedCwd : dirname6(resolvedPath);
3365
+ const manifest = readManifestSync(manifestDir);
3366
+ return {
3367
+ path: resolvedPath,
3368
+ name,
3369
+ factory,
3370
+ manifest
3371
+ };
2965
3372
  }
2966
- var ExtensionRunner = class {
2967
- hooks;
2968
- runtime;
2969
- events;
2970
- cwd;
2971
- abortController = new AbortController();
2972
- _idle = true;
2973
- // Per-extension tracking
2974
- extensions = [];
2975
- // Phase management
2976
- phases = /* @__PURE__ */ new Map();
2977
- _phaseCache = null;
2978
- // Provider management
2979
- pendingProviders = [];
2980
- bound = false;
2981
- // Error listeners
2982
- errorListeners = /* @__PURE__ */ new Set();
2983
- // Loaded extension metadata (pre-initialization form)
2984
- loadedExtensions = [];
2985
- /** Current agent context — set by the agent before each phase */
2986
- currentContext;
3373
+ async function loadExtensions(paths, cwd) {
3374
+ const extensions = [];
3375
+ const errors = [];
3376
+ const resolvedCwd = resolve5(cwd);
3377
+ for (const path of paths) {
3378
+ const resolvedPath = resolve5(resolvedCwd, path);
3379
+ try {
3380
+ const factory = await loadExtensionModule(resolvedPath);
3381
+ if (!factory) {
3382
+ errors.push({
3383
+ path: resolvedPath,
3384
+ error: `Extension does not export a valid factory function: ${path}`
3385
+ });
3386
+ continue;
3387
+ }
3388
+ extensions.push(loadExtensionFromFactory(factory, resolvedCwd, resolvedPath));
3389
+ } catch (error) {
3390
+ errors.push({
3391
+ path: resolvedPath,
3392
+ error: error instanceof Error ? error.message : String(error)
3393
+ });
3394
+ }
3395
+ }
3396
+ return { extensions, errors };
3397
+ }
3398
+ async function loadExtensionsFromPath(path) {
3399
+ const resolvedPath = resolve5(path);
3400
+ const info = await stat2(resolvedPath);
3401
+ if (!info.isDirectory()) {
3402
+ return loadExtensions([resolvedPath], dirname6(resolvedPath));
3403
+ }
3404
+ const entries = await resolveExtensionEntries(resolvedPath) ?? await discoverExtensionsInDir(resolvedPath);
3405
+ return loadExtensions(entries, resolvedPath);
3406
+ }
3407
+
3408
+ // src/agent.ts
3409
+ var Agent = class {
3410
+ state;
3411
+ options;
3412
+ listeners = /* @__PURE__ */ new Set();
3413
+ pendingListenerTasks = /* @__PURE__ */ new Set();
3414
+ listenerErrors = [];
3415
+ activeRun;
3416
+ extensionRunner;
3417
+ loadedExtensions;
3418
+ loadedExtensionsCwd;
3419
+ static loadSkills(targetPath) {
3420
+ return loadSkills(targetPath);
3421
+ }
3422
+ static loadPhases(targetPath) {
3423
+ return loadPhases(targetPath);
3424
+ }
3425
+ static loadExtensions(targetPath) {
3426
+ return loadExtensionsFromPath(targetPath);
3427
+ }
2987
3428
  constructor(options) {
2988
- this.hooks = new HooksManager();
2989
- this.runtime = createExtensionRuntime();
2990
- this.events = createEventBus();
2991
- this.cwd = options?.cwd ?? process.cwd();
3429
+ const context = cloneAgentContext(options.context);
3430
+ this.options = {
3431
+ ...options,
3432
+ context
3433
+ };
3434
+ this.state = {
3435
+ ...this.options.sessionId ? { sessionId: this.options.sessionId } : {},
3436
+ context: cloneAgentContext(context),
3437
+ model: this.options.model,
3438
+ tools: this.options.context.tools ?? [],
3439
+ isRunning: false
3440
+ };
2992
3441
  }
2993
- /** Whether the agent is currently idle (not streaming). */
2994
- get isIdle() {
2995
- return this._idle;
3442
+ subscribe(listener) {
3443
+ this.listeners.add(listener);
3444
+ return () => {
3445
+ this.listeners.delete(listener);
3446
+ };
2996
3447
  }
2997
- /** Set idle state — called by the agent loop. */
2998
- setIdle(idle) {
2999
- this._idle = idle;
3448
+ emitToListeners(event) {
3449
+ for (const listener of this.listeners) {
3450
+ try {
3451
+ const result = listener(event);
3452
+ if (result && typeof result === "object" && "then" in result) {
3453
+ const task = Promise.resolve(result).catch((error) => {
3454
+ this.listenerErrors.push(error);
3455
+ }).finally(() => {
3456
+ this.pendingListenerTasks.delete(task);
3457
+ });
3458
+ this.pendingListenerTasks.add(task);
3459
+ }
3460
+ } catch (error) {
3461
+ this.listenerErrors.push(error);
3462
+ }
3463
+ }
3000
3464
  }
3001
- /** Abort signal for the current runner instance. */
3002
- get signal() {
3003
- return this.abortController.signal;
3465
+ async flushEvents() {
3466
+ while (this.pendingListenerTasks.size > 0) {
3467
+ await Promise.all([...this.pendingListenerTasks]);
3468
+ }
3469
+ for (const listener of this.listeners) {
3470
+ try {
3471
+ await listener.flush?.();
3472
+ } catch (error) {
3473
+ this.listenerErrors.push(error);
3474
+ }
3475
+ }
3476
+ if (this.listenerErrors.length > 0) {
3477
+ const [error] = this.listenerErrors;
3478
+ throw error instanceof Error ? error : new Error(String(error));
3479
+ }
3004
3480
  }
3005
- /** Abort the current runner operation. */
3006
- abort() {
3007
- this.abortController.abort();
3481
+ processEvents(event) {
3482
+ switch (event.type) {
3483
+ case "message_start":
3484
+ this.state.currentResult = void 0;
3485
+ break;
3486
+ case "message_end":
3487
+ break;
3488
+ case "agent_end":
3489
+ break;
3490
+ }
3491
+ this.emitToListeners(event);
3492
+ this.extensionRunner?.emitAgentEvent(event);
3008
3493
  }
3009
- // ---------------------------------------------------------------------------
3010
- // Error handling
3011
- // ---------------------------------------------------------------------------
3012
3494
  /**
3013
- * Register an error listener.
3014
- * Returns an unsubscribe function.
3495
+ * Hook for before_tool_call — called before a tool executes.
3496
+ * Extensions can block execution by setting allow=false.
3015
3497
  */
3016
- onError(listener) {
3017
- this.errorListeners.add(listener);
3018
- return () => this.errorListeners.delete(listener);
3498
+ async handleBeforeToolCall(tool, args) {
3499
+ const runner = this.extensionRunner;
3500
+ if (!runner) return { allow: true };
3501
+ return runner.emitBeforeToolCall(tool, args);
3019
3502
  }
3020
3503
  /**
3021
- * Emit a structured extension error to all listeners.
3504
+ * Hook for after_tool_call — called after a tool executes.
3505
+ * Extensions can mutate the result.
3022
3506
  */
3023
- emitError(error) {
3024
- for (const listener of this.errorListeners) {
3025
- try {
3026
- listener(error);
3027
- } catch (err) {
3028
- console.error("[extension-runner] Error listener failed:", err);
3029
- }
3030
- }
3507
+ async handleAfterToolCall(tool, result) {
3508
+ const runner = this.extensionRunner;
3509
+ if (!runner) return result;
3510
+ return runner.emitAfterToolCall(tool, result);
3031
3511
  }
3032
- // ---------------------------------------------------------------------------
3033
- // Lifecycle: invalidate / assertActive
3034
- // ---------------------------------------------------------------------------
3035
3512
  /**
3036
- * Mark all extension contexts as stale.
3037
- * After calling this, any captured ExtensionAPI or ExtensionContext will throw
3038
- * on use. Used during session replacement or reload.
3513
+ * Hook for before_phase — called before a phase executes.
3514
+ * Extensions can abort, skip, or replace the phase input.
3039
3515
  */
3040
- invalidate(message) {
3041
- this.runtime.invalidate(message);
3516
+ async handleBeforePhase(phaseId, input) {
3517
+ const runner = this.extensionRunner;
3518
+ if (!runner) return {};
3519
+ return runner.emitBeforePhase(phaseId, input);
3042
3520
  }
3043
- // ---------------------------------------------------------------------------
3044
- // Direct hook API
3045
- // ---------------------------------------------------------------------------
3046
3521
  /**
3047
- * Subscribe to a specific hook event type.
3048
- * Returns an unsubscribe function.
3049
- *
3050
- * @example
3051
- * ```ts
3052
- * const unsub = runner.on("before_tool_call", (event) => {
3053
- * return { allow: false, reason: "Blocked" };
3054
- * });
3055
- * unsub(); // Cancel subscription
3056
- * ```
3522
+ * Hook for after_phase — called after a phase executes.
3523
+ * Extensions can abort, retry, or replace the output.
3057
3524
  */
3058
- on(type, handler) {
3059
- this.hooks.on(type, handler);
3060
- return () => this.hooks.off(type, handler);
3525
+ async handleAfterPhase(phaseId, output) {
3526
+ const runner = this.extensionRunner;
3527
+ if (!runner) return {};
3528
+ return runner.emitAfterPhase(phaseId, output);
3529
+ }
3530
+ /**
3531
+ * Hook for before_prompt — called before model request, allowing extensions to transform PhaseContext.
3532
+ * Extensions can transform the PhaseContext (messages, tools, systemPrompt, etc.).
3533
+ */
3534
+ async handleBeforePrompt(phaseId, input) {
3535
+ const runner = this.extensionRunner;
3536
+ if (!runner) return input;
3537
+ return runner.emitBeforePrompt(phaseId, input);
3538
+ }
3539
+ handleRunFailure(error, aborted) {
3540
+ const message = error instanceof Error ? error.message : "Agent run failed.";
3541
+ this.state.error = message;
3542
+ }
3543
+ async runWithLifecycle(executor) {
3544
+ if (this.activeRun) {
3545
+ throw new Error("Agent is already running.");
3546
+ }
3547
+ let resolvePromise;
3548
+ const abortController = new AbortController();
3549
+ const promise = new Promise((resolve7) => {
3550
+ resolvePromise = resolve7;
3551
+ });
3552
+ this.activeRun = { promise, resolve: resolvePromise, abortController };
3553
+ this.state.isRunning = true;
3554
+ try {
3555
+ const result = await executor(abortController.signal);
3556
+ resolvePromise(result);
3557
+ return result;
3558
+ } catch (error) {
3559
+ this.handleRunFailure(error, abortController.signal.aborted);
3560
+ resolvePromise(void 0);
3561
+ throw error;
3562
+ } finally {
3563
+ this.finishRun();
3564
+ }
3565
+ }
3566
+ finishRun() {
3567
+ this.state.isRunning = false;
3568
+ this.activeRun = void 0;
3569
+ }
3570
+ async loadExtensions(extensions, cwd) {
3571
+ if (extensions.length === 0) {
3572
+ this.extensionRunner?.invalidate();
3573
+ this.extensionRunner = void 0;
3574
+ this.loadedExtensions = extensions;
3575
+ this.loadedExtensionsCwd = cwd;
3576
+ return;
3577
+ }
3578
+ if (this.extensionRunner && this.loadedExtensions === extensions && this.loadedExtensionsCwd === cwd) {
3579
+ return;
3580
+ }
3581
+ const runner = createExtensionRunner({ cwd });
3582
+ await runner.loadExtensions(extensions);
3583
+ runner.bind();
3584
+ this.extensionRunner?.invalidate();
3585
+ this.extensionRunner = runner;
3586
+ this.loadedExtensions = extensions;
3587
+ this.loadedExtensionsCwd = cwd;
3588
+ }
3589
+ async run(config) {
3590
+ const resolved = this.resolveRunConfig(config);
3591
+ const extensions = resolved.extensions ?? [];
3592
+ await this.loadExtensions(extensions, resolved.cwd);
3593
+ const runContext = cloneAgentContext(resolved.context, this.extensionRunner);
3594
+ const sessionId = resolved.sessionId ?? this.state.sessionId;
3595
+ this.options = resolved;
3596
+ if (sessionId) {
3597
+ this.state.sessionId = sessionId;
3598
+ }
3599
+ this.state.context = runContext;
3600
+ this.state.model = resolved.model;
3601
+ this.state.tools = runContext.tools ?? [];
3602
+ this.state.currentResult = void 0;
3603
+ this.state.error = void 0;
3604
+ return this.runWithLifecycle(async (signal) => {
3605
+ const emit = (event) => {
3606
+ this.processEvents(event);
3607
+ };
3608
+ const beforeToolCall = async (input) => {
3609
+ if (resolved.beforeToolCall) {
3610
+ const userResult = await resolved.beforeToolCall(input);
3611
+ if (!userResult.allow) return userResult;
3612
+ }
3613
+ const extResult = await this.handleBeforeToolCall(input.tool, input.args);
3614
+ if (!extResult.allow) {
3615
+ return { allow: false, reason: extResult.reason ?? "Blocked by extension" };
3616
+ }
3617
+ return { allow: true };
3618
+ };
3619
+ const afterToolCall = async (input) => {
3620
+ let result2 = input.result;
3621
+ if (resolved.afterToolCall) {
3622
+ result2 = await resolved.afterToolCall({ tool: input.tool, result: result2 });
3623
+ }
3624
+ return this.handleAfterToolCall(input.tool, result2);
3625
+ };
3626
+ const result = await runAgentLoop({
3627
+ context: runContext,
3628
+ sessionId: sessionId ?? createId("ses"),
3629
+ model: resolved.model,
3630
+ stream: resolved.stream,
3631
+ maxAttempts: resolved.maxAttempts,
3632
+ signal,
3633
+ beforeToolCall,
3634
+ afterToolCall,
3635
+ beforePhase: (phaseId, input) => this.handleBeforePhase(phaseId, input),
3636
+ afterPhase: (phaseId, output) => this.handleAfterPhase(phaseId, output),
3637
+ beforePrompt: (phaseId, input) => this.handleBeforePrompt(phaseId, input),
3638
+ emit,
3639
+ onMessage: resolved.onMessage,
3640
+ onOutcome: resolved.onOutcome,
3641
+ onModelTranscript: resolved.onModelTranscript
3642
+ });
3643
+ const nextContext = {
3644
+ ...cloneAgentContext(resolved.context),
3645
+ messages: snapshotMessages(result.messages)
3646
+ };
3647
+ this.state.sessionId = result.sessionId;
3648
+ this.state.context = cloneAgentContext(nextContext, this.extensionRunner);
3649
+ this.state.currentResult = result;
3650
+ this.options = {
3651
+ ...resolved,
3652
+ sessionId: result.sessionId,
3653
+ context: nextContext
3654
+ };
3655
+ return result;
3656
+ });
3657
+ }
3658
+ abort(reason = "Aborted by caller.") {
3659
+ this.activeRun?.abortController.abort(reason);
3061
3660
  }
3062
3661
  /**
3063
- * Subscribe to all events (read-only).
3064
- * Returns an unsubscribe function.
3662
+ * Get formatted skill content for LLM consumption.
3065
3663
  *
3066
- * @example
3067
- * ```ts
3068
- * const unsub = runner.subscribe((event) => {
3069
- * console.log(event.type);
3070
- * });
3071
- * ```
3664
+ * Finds the skill by name and returns the formatted content using `formatSkillInvocation`.
3665
+ * This is a programmatic API for developers to invoke skills directly.
3666
+ *
3667
+ * @param name - The skill name to look up
3668
+ * @param additionalInstructions - Optional additional instructions to append
3669
+ * @returns Formatted skill content string
3072
3670
  */
3073
- subscribe(listener) {
3074
- const handlers = /* @__PURE__ */ new Map();
3075
- for (const eventType of this.getAllEventTypes()) {
3076
- const handler = (event) => listener(event);
3077
- handlers.set(eventType, handler);
3078
- this.hooks.on(eventType, handler);
3671
+ skill(name, additionalInstructions) {
3672
+ const skill = this.state.context.skills.find((s) => s.name === name);
3673
+ if (!skill) {
3674
+ throw new Error(`Unknown skill: ${name}`);
3079
3675
  }
3080
- return () => {
3081
- for (const [eventType, handler] of handlers) {
3082
- this.hooks.off(eventType, handler);
3083
- }
3084
- };
3085
- }
3086
- getAllEventTypes() {
3087
- return [
3088
- "before_phase",
3089
- "after_phase",
3090
- "before_prompt",
3091
- "before_tool_call",
3092
- "after_tool_call",
3093
- "agent_start",
3094
- "agent_end",
3095
- "turn_start",
3096
- "turn_end",
3097
- "message_start",
3098
- "message_update",
3099
- "message_end",
3100
- "tool_execution_start",
3101
- "tool_execution_update",
3102
- "tool_execution_end",
3103
- "queue_update",
3104
- "save_point",
3105
- "abort",
3106
- "settled"
3107
- ];
3676
+ const content = readSkillContent(skill);
3677
+ return additionalInstructions ? `${content}
3678
+
3679
+ ${additionalInstructions}` : content;
3108
3680
  }
3109
- // ---------------------------------------------------------------------------
3110
- // Extension loading
3111
- // ---------------------------------------------------------------------------
3112
3681
  /**
3113
- * Load and initialize extensions.
3114
- * Creates Extension tracking objects and calls each factory with an ExtensionAPI.
3682
+ * Get formatted phase content for LLM consumption.
3683
+ *
3684
+ * Finds the phase by name and returns the formatted content.
3685
+ * This is a programmatic API for developers to invoke phases directly.
3686
+ *
3687
+ * @param name - The phase name to look up
3688
+ * @returns Formatted phase content string, or empty string if not found
3115
3689
  */
3116
- async loadExtensions(extensions) {
3117
- for (const ext of extensions) {
3118
- try {
3119
- const sourceInfo = createSourceInfo(ext.path, {
3120
- source: ext.path.startsWith("<builtin:") ? "builtin" : "local",
3121
- baseDir: ext.resolvedPath.startsWith("<") ? void 0 : ext.resolvedPath
3122
- });
3123
- const extension = createExtension(ext.path, ext.resolvedPath, sourceInfo);
3124
- const api = this.createExtensionAPI(extension, ext.manifest);
3125
- await ext.factory(api);
3126
- this.extensions.push(extension);
3127
- this.loadedExtensions.push(ext);
3128
- this._phaseCache = null;
3129
- } catch (error) {
3130
- const message = error instanceof Error ? error.message : String(error);
3131
- this.emitError({
3132
- extensionPath: ext.path,
3133
- event: "load",
3134
- error: message,
3135
- stack: error instanceof Error ? error.stack : void 0
3136
- });
3137
- throw error;
3138
- }
3139
- }
3690
+ async phase(name) {
3691
+ await this.loadExtensions(this.options.extensions ?? [], this.options.cwd);
3692
+ const registry = cloneAgentContext(this.options.context, this.extensionRunner).phases?.phases;
3693
+ const phase = registry?.get(name) ?? [...registry?.values() ?? []].find((candidate) => candidate.name === name);
3694
+ return phase ? readPhaseContent(phase) : "";
3140
3695
  }
3141
- // ---------------------------------------------------------------------------
3142
- // Tool management
3143
- // ---------------------------------------------------------------------------
3144
- /**
3145
- * Get all registered tools from all extensions (first registration per name wins).
3146
- */
3147
- getAllRegisteredTools() {
3148
- const toolsByName = /* @__PURE__ */ new Map();
3149
- for (const ext of this.extensions) {
3150
- for (const tool of ext.tools.values()) {
3151
- if (!toolsByName.has(tool.definition.name)) {
3152
- toolsByName.set(tool.definition.name, tool);
3153
- }
3154
- }
3696
+ async waitForIdle() {
3697
+ if (!this.activeRun) {
3698
+ return;
3155
3699
  }
3156
- return Array.from(toolsByName.values());
3700
+ await this.activeRun.promise.catch(() => void 0);
3701
+ await this.flushEvents().catch(() => void 0);
3157
3702
  }
3158
- /**
3159
- * Get a tool definition by name. Returns undefined if not found.
3160
- */
3161
- getToolDefinition(toolName) {
3162
- for (const ext of this.extensions) {
3163
- const tool = ext.tools.get(toolName);
3164
- if (tool) return tool.definition;
3165
- }
3166
- return void 0;
3703
+ resolveRunConfig(config) {
3704
+ const merged = {
3705
+ ...this.options,
3706
+ ...config
3707
+ };
3708
+ const context = cloneAgentContext(config?.context ?? this.createContextSnapshot());
3709
+ return {
3710
+ ...merged,
3711
+ context,
3712
+ sessionId: config?.sessionId ?? this.state.sessionId ?? this.options.sessionId
3713
+ };
3167
3714
  }
3168
- // ---------------------------------------------------------------------------
3169
- // Handler queries
3170
- // ---------------------------------------------------------------------------
3171
- /**
3172
- * Check if there are handlers registered for specified event type.
3173
- */
3174
- hasHandlers(eventType) {
3175
- return this.hooks.has(eventType);
3715
+ createContextSnapshot() {
3716
+ return cloneAgentContext(this.options.context);
3176
3717
  }
3177
- /**
3178
- * Get the number of handlers for specified event type.
3179
- */
3180
- handlerCount(eventType) {
3181
- return this.hooks.count(eventType);
3718
+ };
3719
+ function cloneAgentContext(context, extensionRunner) {
3720
+ const phases = /* @__PURE__ */ new Map();
3721
+ phases.set(DEFAULT_PHASE_ID, createDefaultPhase());
3722
+ for (const [id, phase] of context.phases?.phases ?? []) {
3723
+ phases.set(id, phase);
3182
3724
  }
3183
- // ---------------------------------------------------------------------------
3184
- // Phase management
3185
- // ---------------------------------------------------------------------------
3186
- getPhase(id) {
3187
- const reg = this.getRegisteredPhase(id);
3188
- if (!reg) return void 0;
3189
- return this.adaptToPhase(reg);
3725
+ const extensionRegistry = extensionRunner?.createPhaseRegistry();
3726
+ for (const [id, phase] of extensionRegistry?.phases ?? []) {
3727
+ phases.set(id, phase);
3190
3728
  }
3191
- getPhases() {
3192
- return [...this.collectRegisteredPhases().values()].map(
3193
- (p) => this.adaptToPhase(p)
3194
- );
3729
+ return {
3730
+ systemPrompt: context.systemPrompt,
3731
+ messages: snapshotMessages(context.messages),
3732
+ tools: context.tools.slice(),
3733
+ skills: context.skills.slice(),
3734
+ phases: {
3735
+ phases,
3736
+ entryPhaseId: extensionRegistry?.entryPhaseId ?? context.phases?.entryPhaseId ?? DEFAULT_PHASE_ID
3737
+ }
3738
+ };
3739
+ }
3740
+
3741
+ // src/harness/session/session.ts
3742
+ import Type3 from "typebox";
3743
+ var SESSION_SCHEMA_VERSION = "0.4.4";
3744
+ var AgentMessageSchema = Type3.Object({
3745
+ id: Type3.String(),
3746
+ role: Type3.Union([
3747
+ Type3.Literal("system"),
3748
+ Type3.Literal("user"),
3749
+ Type3.Literal("assistant"),
3750
+ Type3.Literal("tool")
3751
+ ]),
3752
+ content: Type3.Union([Type3.String(), Type3.Array(Type3.Unknown())]),
3753
+ createdAt: Type3.String(),
3754
+ metadata: Type3.Optional(Type3.Record(Type3.String(), Type3.Unknown()))
3755
+ });
3756
+ var SkillSchema = Type3.Object({
3757
+ name: Type3.String(),
3758
+ description: Type3.String(),
3759
+ filePath: Type3.String(),
3760
+ baseDir: Type3.String(),
3761
+ content: Type3.String(),
3762
+ disableModelInvocation: Type3.Boolean()
3763
+ });
3764
+ function createMessage2(role, content, metadata) {
3765
+ return {
3766
+ id: createId("msg"),
3767
+ role,
3768
+ content,
3769
+ createdAt: createTimestamp(),
3770
+ ...metadata ? { metadata } : {}
3771
+ };
3772
+ }
3773
+ function createSession(input) {
3774
+ const createdAt = createTimestamp();
3775
+ const messages = [
3776
+ createMessage2("user", input.input)
3777
+ ];
3778
+ return {
3779
+ version: SESSION_SCHEMA_VERSION,
3780
+ id: input.id ?? createId("ses"),
3781
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
3782
+ systemPrompt: input.systemPrompt,
3783
+ input: input.input,
3784
+ messages,
3785
+ log: [],
3786
+ skills: input.skills ?? [],
3787
+ createdAt,
3788
+ updatedAt: createdAt,
3789
+ ...input.title ? { title: input.title } : {}
3790
+ };
3791
+ }
3792
+ function appendUserTurn(session, input) {
3793
+ session.messages.push(createMessage2("user", input));
3794
+ session.updatedAt = createTimestamp();
3795
+ return session;
3796
+ }
3797
+
3798
+ // src/harness/session/session-manager.ts
3799
+ var SESSION_MANAGER_SCHEMA_VERSION = "0.4.4";
3800
+ function clone(value) {
3801
+ return JSON.parse(JSON.stringify(value));
3802
+ }
3803
+ function createSessionHeader(input) {
3804
+ const createdAt = createTimestamp();
3805
+ return {
3806
+ type: "header",
3807
+ id: input.id ?? createId("ses"),
3808
+ version: SESSION_MANAGER_SCHEMA_VERSION,
3809
+ createdAt,
3810
+ updatedAt: createdAt,
3811
+ systemPrompt: input.systemPrompt,
3812
+ input: input.input,
3813
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
3814
+ skills: input.skills?.map(clone) ?? [],
3815
+ ...input.title ? { title: input.title } : {},
3816
+ currentLeafId: null
3817
+ };
3818
+ }
3819
+ function filterExecutionTurns(steps, filter = {}) {
3820
+ return steps.filter((step) => {
3821
+ if (filter.phase && step.phase !== filter.phase) return false;
3822
+ if (filter.afterMs !== void 0 && step.requestedAtMs < filter.afterMs) return false;
3823
+ return true;
3824
+ }).map(clone);
3825
+ }
3826
+ var InMemorySessionManager = class _InMemorySessionManager {
3827
+ constructor(header, entries = []) {
3828
+ this.header = header;
3829
+ this.entries = entries;
3830
+ }
3831
+ header;
3832
+ entries;
3833
+ static create(input) {
3834
+ return new _InMemorySessionManager(createSessionHeader(input));
3195
3835
  }
3196
- createPhaseRegistry(input = {}) {
3197
- const registered = this.collectRegisteredPhases();
3198
- const phases = /* @__PURE__ */ new Map();
3199
- for (const [id, reg] of registered) {
3200
- phases.set(id, this.adaptToPhase(reg));
3836
+ static fromRecords(records) {
3837
+ const [header, ...entries] = records;
3838
+ if (!header || header.type !== "header") {
3839
+ throw new Error("Session records must start with a header.");
3201
3840
  }
3202
- const entryPhaseId = input.entryPhaseId ?? null;
3203
- return { phases, entryPhaseId };
3841
+ return new _InMemorySessionManager(clone(header), clone(entries));
3204
3842
  }
3205
- /** Adapt an extension RegisteredPhase to the core Phase type. */
3206
- adaptToPhase(reg) {
3207
- const def = reg.definition;
3208
- return {
3209
- id: def.id,
3210
- name: def.name ?? def.id,
3211
- description: def.description ?? "",
3212
- tools: def.tools,
3213
- skills: def.skills,
3214
- target: def.target,
3215
- input: def.input,
3216
- run: def.run,
3217
- filePath: "",
3218
- baseDir: "",
3219
- content: "",
3220
- ...def.model ? { model: parseModelRef(def.model) } : {}
3221
- };
3843
+ getSessionId() {
3844
+ return this.header.id;
3222
3845
  }
3223
- // ---------------------------------------------------------------------------
3224
- // Lifecycle
3225
- // ---------------------------------------------------------------------------
3226
- /**
3227
- * Bind the runner — flushes pending provider registrations and
3228
- * replaces runtime stubs with real implementations.
3229
- */
3230
- bind() {
3231
- if (this.bound) return;
3232
- this.bound = true;
3233
- this.flushPendingProviders();
3234
- this.runtime.registerProvider = (_name, config) => {
3235
- applyProviderRegistration(config);
3236
- };
3237
- this.runtime.unregisterProvider = (_name) => {
3238
- applyProviderUnregistration(_name);
3239
- };
3846
+ getSessionFile() {
3847
+ return void 0;
3240
3848
  }
3241
- // ---------------------------------------------------------------------------
3242
- // Unified hook emission
3243
- // ---------------------------------------------------------------------------
3244
- /**
3245
- * Generic emit — fire-and-forget for any event type.
3246
- */
3247
- async emit(eventType, event) {
3248
- await this.hooks.emit(eventType, event);
3849
+ async getHeader() {
3850
+ return clone(this.header);
3249
3851
  }
3250
- /**
3251
- * Unified hook emission — returns the first non-undefined result.
3252
- */
3253
- async emitHook(type, event) {
3254
- return this.hooks.emitFirst(type, event);
3852
+ async appendMessage(message) {
3853
+ return this.appendEntry({ type: "message", message: clone(message) });
3255
3854
  }
3256
- // ---------------------------------------------------------------------------
3257
- // Phase hooks (with inline processing)
3258
- // ---------------------------------------------------------------------------
3259
- async emitBeforePhase(phaseId, input) {
3260
- const result = await this.emitHook("before_phase", {
3261
- type: "before_phase",
3262
- phaseId,
3263
- input
3264
- });
3265
- return result ?? {};
3855
+ async appendOutcome(outcome) {
3856
+ return this.appendEntry({ type: "outcome", outcome: clone(outcome) });
3266
3857
  }
3267
- async emitAfterPhase(phaseId, output) {
3268
- const result = await this.emitHook("after_phase", {
3269
- type: "after_phase",
3270
- phaseId,
3271
- output
3858
+ async appendExecutionTurn(turn) {
3859
+ return this.appendEntry({
3860
+ type: "execution_turn",
3861
+ turn: clone({
3862
+ ...turn,
3863
+ sessionId: this.header.id
3864
+ })
3272
3865
  });
3273
- return result ?? {};
3274
3866
  }
3275
- async emitBeforePrompt(phaseId, input) {
3276
- const result = await this.emitHook("before_prompt", {
3277
- type: "before_prompt",
3278
- phaseId,
3279
- input
3280
- });
3281
- return result?.input ?? input;
3867
+ async appendCompaction(input) {
3868
+ return this.appendEntry({ type: "compaction", ...input });
3282
3869
  }
3283
- async emitBeforeToolCall(tool, args) {
3284
- const result = await this.emitHook("before_tool_call", {
3285
- type: "before_tool_call",
3286
- tool,
3287
- args
3288
- });
3289
- return result ?? { allow: true };
3870
+ async appendBranchSummary(input) {
3871
+ return this.appendEntry({ type: "branch_summary", ...input });
3290
3872
  }
3291
- async emitAfterToolCall(tool, result) {
3292
- const hookResult = await this.emitHook("after_tool_call", {
3293
- type: "after_tool_call",
3294
- tool,
3295
- result
3873
+ async appendSessionInfo(input) {
3874
+ this.header = {
3875
+ ...this.header,
3876
+ title: input.title,
3877
+ updatedAt: createTimestamp()
3878
+ };
3879
+ return this.appendEntry({ type: "session_info", title: input.title });
3880
+ }
3881
+ async appendCustom(input) {
3882
+ return this.appendEntry({ type: "custom", customType: input.customType, data: clone(input.data) });
3883
+ }
3884
+ async appendSessionState(state) {
3885
+ return this.appendEntry({ type: "session_state", state: clone(state) });
3886
+ }
3887
+ async appendModelTranscript(transcript, meta) {
3888
+ return this.appendEntry({
3889
+ type: "model_transcript",
3890
+ transcript: clone(transcript),
3891
+ ...meta?.phase ? { phase: meta.phase } : {},
3892
+ ...meta?.model ? { model: clone(meta.model) } : {}
3296
3893
  });
3297
- return hookResult?.result ?? result;
3298
3894
  }
3299
- // ---------------------------------------------------------------------------
3300
- // Agent event hooks (fire-and-forget)
3301
- // ---------------------------------------------------------------------------
3302
- async emitAgentStart(sessionId) {
3303
- await this.hooks.emit("agent_start", { type: "agent_start", sessionId });
3895
+ async getSessionState() {
3896
+ for (let i = this.entries.length - 1; i >= 0; i--) {
3897
+ const entry = this.entries[i];
3898
+ if (entry.type === "session_state") {
3899
+ return clone(entry.state);
3900
+ }
3901
+ }
3902
+ return void 0;
3304
3903
  }
3305
- async emitAgentEnd(sessionId, outcome, messages) {
3306
- await this.hooks.emit("agent_end", { type: "agent_end", sessionId, outcome, messages });
3904
+ async branch(entryId) {
3905
+ if (entryId !== null && !this.entries.some((entry) => entry.id === entryId)) {
3906
+ throw new Error(`Session entry not found: ${entryId}`);
3907
+ }
3908
+ this.header = {
3909
+ ...this.header,
3910
+ currentLeafId: entryId,
3911
+ updatedAt: createTimestamp()
3912
+ };
3307
3913
  }
3308
- async emitTurnStart(messages) {
3309
- await this.hooks.emit("turn_start", { type: "turn_start", messages });
3914
+ async buildAgentContext(input = {}) {
3915
+ const entries = this.entriesForLeaf(input.leafId ?? this.header.currentLeafId ?? null);
3916
+ return {
3917
+ systemPrompt: this.header.systemPrompt,
3918
+ messages: entries.filter((entry) => entry.type === "message").map((entry) => entry.message).map(clone),
3919
+ tools: input.tools?.slice() ?? [],
3920
+ skills: input.skills?.map(clone) ?? this.header.skills.map(clone)
3921
+ };
3310
3922
  }
3311
- async emitTurnEnd(messages, outcome) {
3312
- await this.hooks.emit("turn_end", { type: "turn_end", messages, outcome });
3923
+ async listEntries() {
3924
+ return this.entries.map(clone);
3313
3925
  }
3314
- async emitMessageStart(message) {
3315
- await this.hooks.emit("message_start", { type: "message_start", message });
3926
+ async loadExecutionTurns(filter) {
3927
+ return filterExecutionTurns(
3928
+ this.entries.filter((entry) => entry.type === "execution_turn").map((entry) => entry.turn),
3929
+ filter
3930
+ );
3316
3931
  }
3317
- async emitMessageUpdate(message, delta) {
3318
- await this.hooks.emit("message_update", { type: "message_update", message, delta });
3932
+ appendImportedEntry(entry) {
3933
+ this.entries.push(clone(entry));
3934
+ this.header = {
3935
+ ...this.header,
3936
+ currentLeafId: entry.id,
3937
+ updatedAt: entry.timestamp
3938
+ };
3319
3939
  }
3320
- async emitMessageEnd(message) {
3321
- await this.hooks.emit("message_end", { type: "message_end", message });
3940
+ appendEntry(input) {
3941
+ const timestamp = createTimestamp();
3942
+ const entry = {
3943
+ id: createId("entry"),
3944
+ parentId: this.header.currentLeafId ?? null,
3945
+ timestamp,
3946
+ ...input
3947
+ };
3948
+ this.entries.push(entry);
3949
+ this.header = {
3950
+ ...this.header,
3951
+ currentLeafId: entry.id,
3952
+ updatedAt: timestamp
3953
+ };
3954
+ return entry.id;
3322
3955
  }
3323
- async emitToolExecutionStart(toolCallId, toolName, args) {
3324
- await this.hooks.emit("tool_execution_start", {
3325
- type: "tool_execution_start",
3326
- toolCallId,
3327
- toolName,
3328
- args
3329
- });
3956
+ entriesForLeaf(leafId) {
3957
+ if (!leafId) {
3958
+ return [];
3959
+ }
3960
+ const byId = new Map(this.entries.map((entry) => [entry.id, entry]));
3961
+ const ordered = [];
3962
+ let currentId = leafId;
3963
+ while (currentId) {
3964
+ const entry = byId.get(currentId);
3965
+ if (!entry) {
3966
+ throw new Error(`Session entry not found: ${currentId}`);
3967
+ }
3968
+ ordered.unshift(entry);
3969
+ currentId = entry.parentId;
3970
+ }
3971
+ return ordered;
3330
3972
  }
3331
- async emitToolExecutionUpdate(toolCallId, toolName, _progress) {
3332
- await this.hooks.emit("tool_execution_update", {
3333
- type: "tool_execution_update",
3334
- toolCallId,
3335
- toolName
3336
- });
3973
+ };
3974
+ function summarizeSessionManagerRecords(records) {
3975
+ const [header, ...entries] = records;
3976
+ if (!header || header.type !== "header") {
3977
+ throw new Error("Session records must start with a header.");
3978
+ }
3979
+ const messages = entries.filter((entry) => entry.type === "message").map((entry) => entry.message);
3980
+ const latestMessageContent = messages.at(-1)?.content;
3981
+ const latestMessage = latestMessageContent ? messageContentText(latestMessageContent) : void 0;
3982
+ return {
3983
+ id: header.id,
3984
+ ...header.title ? { title: header.title } : {},
3985
+ createdAt: header.createdAt,
3986
+ updatedAt: header.updatedAt,
3987
+ messageCount: messages.length,
3988
+ ...latestMessage ? { latestMessage } : {}
3989
+ };
3990
+ }
3991
+
3992
+ // src/harness/session/jsonl.ts
3993
+ import { appendFile, mkdir as mkdir2, readFile as readFile5, readdir as readdir4, stat as stat3, unlink, writeFile as writeFile2 } from "fs/promises";
3994
+ import { join as join6, relative as relative3, resolve as resolve6, sep as sep3 } from "path";
3995
+ var SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_-]+$/;
3996
+ function isPathInside(parent, child) {
3997
+ const relativePath = relative3(parent, child);
3998
+ return Boolean(relativePath) && !relativePath.startsWith("..") && !relativePath.includes(`..${sep3}`);
3999
+ }
4000
+ function safeSessionPath(sessionsDir, id) {
4001
+ if (!SESSION_ID_PATTERN.test(id)) {
4002
+ return void 0;
3337
4003
  }
3338
- async emitToolExecutionEnd(toolCallId, toolName, result) {
3339
- await this.hooks.emit("tool_execution_end", {
3340
- type: "tool_execution_end",
3341
- toolCallId,
3342
- toolName,
3343
- result
3344
- });
4004
+ const root = resolve6(sessionsDir);
4005
+ const path = resolve6(root, `${id}.jsonl`);
4006
+ return isPathInside(root, path) ? path : void 0;
4007
+ }
4008
+ function parseRecord(line) {
4009
+ const value = JSON.parse(line);
4010
+ if (!value || typeof value !== "object" || !("type" in value)) {
4011
+ throw new Error("Invalid session JSONL record.");
3345
4012
  }
3346
- async emitSavePoint(hadPendingMutations) {
3347
- await this.hooks.emit("save_point", { type: "save_point", hadPendingMutations });
4013
+ return value;
4014
+ }
4015
+ async function readRecords(path) {
4016
+ const text = await readFile5(path, "utf8").catch((error) => {
4017
+ if (error.code === "ENOENT") {
4018
+ return void 0;
4019
+ }
4020
+ throw error;
4021
+ });
4022
+ if (!text) {
4023
+ return void 0;
3348
4024
  }
3349
- async emitAbort(reason) {
3350
- await this.hooks.emit("abort", { type: "abort", reason });
4025
+ const records = text.split("\n").map((line) => line.trim()).filter(Boolean).map(parseRecord);
4026
+ const header = records[0];
4027
+ if (!header || header.type !== "header") {
4028
+ throw new Error("Session JSONL must start with a header record.");
3351
4029
  }
3352
- async emitSettled() {
3353
- await this.hooks.emit("settled", { type: "settled" });
4030
+ const lastEntry = records.slice(1).reverse().find((record) => record.type !== "header");
4031
+ return [
4032
+ {
4033
+ ...header,
4034
+ updatedAt: lastEntry?.timestamp ?? header.updatedAt,
4035
+ currentLeafId: lastEntry?.id ?? header.currentLeafId ?? null
4036
+ },
4037
+ ...records.slice(1)
4038
+ ];
4039
+ }
4040
+ async function appendRecord(path, record) {
4041
+ await appendFile(path, `${JSON.stringify(record)}
4042
+ `, "utf8");
4043
+ }
4044
+ var LocalJsonlSessionManager = class _LocalJsonlSessionManager {
4045
+ constructor(sessionsDir, filePath, inner) {
4046
+ this.sessionsDir = sessionsDir;
4047
+ this.filePath = filePath;
4048
+ this.inner = inner;
3354
4049
  }
3355
- // ---------------------------------------------------------------------------
3356
- // AgentEvent bridge (backward compatibility)
3357
- // ---------------------------------------------------------------------------
3358
- /**
3359
- * Emit an AgentEvent by routing to the appropriate typed hook.
3360
- */
3361
- async emitAgentEvent(event) {
3362
- switch (event.type) {
3363
- case "agent_start":
3364
- await this.emitAgentStart(event.sessionId);
3365
- break;
3366
- case "agent_end":
3367
- await this.emitAgentEnd(event.sessionId, event.outcome, event.messages);
3368
- break;
3369
- case "turn_start":
3370
- await this.emitTurnStart(event.messages);
3371
- break;
3372
- case "turn_end":
3373
- await this.emitTurnEnd(event.messages, event.outcome);
3374
- break;
3375
- case "message_start":
3376
- await this.emitMessageStart(event.message);
3377
- break;
3378
- case "message_update":
3379
- await this.emitMessageUpdate(event.message, event.delta);
3380
- break;
3381
- case "message_end":
3382
- await this.emitMessageEnd(event.message);
3383
- break;
3384
- case "tool_execution_start":
3385
- await this.emitToolExecutionStart(event.toolCallId, event.toolName, event.args);
3386
- break;
3387
- case "tool_execution_update":
3388
- await this.emitToolExecutionUpdate(event.toolCallId, event.toolName);
3389
- break;
3390
- case "tool_execution_end":
3391
- await this.emitToolExecutionEnd(event.toolCallId, event.toolName, event.result);
3392
- break;
4050
+ sessionsDir;
4051
+ filePath;
4052
+ inner;
4053
+ static async create(sessionsDir, input) {
4054
+ const header = createSessionHeader(input);
4055
+ const path = safeSessionPath(sessionsDir, header.id);
4056
+ if (!path) {
4057
+ throw new Error(`Invalid session id: ${header.id}`);
3393
4058
  }
3394
- }
3395
- // ---------------------------------------------------------------------------
3396
- // Internal helpers
3397
- // ---------------------------------------------------------------------------
3398
- /**
3399
- * Create an ExtensionAPI for a specific extension.
3400
- * Registration methods write to the extension tracking object.
3401
- * Action methods delegate to the shared runtime.
3402
- */
3403
- createExtensionAPI(extension, manifest) {
3404
- const runner = this;
3405
- const extContext = {
3406
- get cwd() {
3407
- return runner.cwd;
3408
- },
3409
- get signal() {
3410
- return runner.abortController.signal;
3411
- },
3412
- isIdle() {
3413
- return runner._idle;
3414
- },
3415
- abort() {
3416
- runner.abortController.abort();
3417
- },
3418
- exec(command, args, options) {
3419
- return execCommand(command, args, runner.cwd, options);
3420
- },
3421
- manifest,
3422
- getSystemPrompt() {
3423
- return runner.currentContext?.systemPrompt ?? "";
3424
- },
3425
- setSystemPrompt(prompt) {
3426
- if (runner.currentContext) runner.currentContext.systemPrompt = prompt;
3427
- },
3428
- getMessages() {
3429
- return runner.currentContext?.messages ?? [];
3430
- },
3431
- addMessage(role, content) {
3432
- runner.currentContext?.messages.push({ role, content });
3433
- },
3434
- getAvailableTools() {
3435
- return (runner.currentContext?.tools ?? []).map((t) => ({ name: t.name, description: t.description }));
3436
- },
3437
- getAvailableSkills() {
3438
- return (runner.currentContext?.skills ?? []).map((s) => ({ name: s.name, description: s.description }));
3439
- },
3440
- getSkillContent(skillName) {
3441
- const skill = runner.currentContext?.skills.find((s) => s.name === skillName);
3442
- return skill?.content ?? "";
3443
- },
3444
- getAvailablePhases() {
3445
- return [...runner.phases.keys()];
3446
- },
3447
- getPhaseContent(phaseId) {
3448
- return runner.phases.get(phaseId)?.definition.description ?? "";
3449
- }
3450
- };
3451
- return createExtensionAPI(this.hooks, extension.path, {
3452
- registerPhase: (registration) => this.registerPhase(extension, registration),
3453
- registerProvider: (config) => this.registerProvider(config),
3454
- unregisterProvider: (name) => this.unregisterProvider(name),
3455
- registerTool: (tool) => this.registerTool(extension, tool),
3456
- context: extContext,
3457
- manifest
3458
- }, this.runtime, this.events);
3459
- }
3460
- registerTool(extension, tool) {
3461
- for (const ext of this.extensions) {
3462
- if (ext.tools.has(tool.name)) {
3463
- this.emitError({
3464
- extensionPath: extension.path,
3465
- event: "register_tool",
3466
- error: `Tool "${tool.name}" is already registered by extension ${ext.path}`
3467
- });
3468
- return;
4059
+ const exists = await stat3(path).then(() => true).catch((error) => {
4060
+ if (error.code === "ENOENT") {
4061
+ return false;
3469
4062
  }
3470
- }
3471
- const sourceInfo = createSourceInfo(extension.path);
3472
- extension.tools.set(tool.name, {
3473
- definition: tool,
3474
- sourceInfo
4063
+ throw error;
3475
4064
  });
3476
- }
3477
- registerPhase(extension, registration) {
3478
- if (!registration.id) {
3479
- throw new Error(`Phase registration requires an "id" field.`);
4065
+ if (exists) {
4066
+ throw new Error(`Session already exists: ${header.id}`);
3480
4067
  }
3481
- if (!registration.name) {
3482
- throw new Error(`Phase registration "${registration.id}" requires a "name" field.`);
4068
+ await mkdir2(sessionsDir, { recursive: true });
4069
+ await writeFile2(path, `${JSON.stringify(header)}
4070
+ `, "utf8");
4071
+ return new _LocalJsonlSessionManager(
4072
+ sessionsDir,
4073
+ path,
4074
+ InMemorySessionManager.fromRecords([header])
4075
+ );
4076
+ }
4077
+ static async open(sessionsDir, id) {
4078
+ const path = safeSessionPath(sessionsDir, id);
4079
+ if (!path) {
4080
+ return void 0;
3483
4081
  }
3484
- if (!registration.description) {
3485
- throw new Error(`Phase registration "${registration.id}" requires a "description" field.`);
4082
+ const records = await readRecords(path);
4083
+ if (!records) {
4084
+ return void 0;
3486
4085
  }
3487
- if (this.phases.has(registration.id)) {
3488
- throw new Error(`Duplicate phase id: ${registration.id}`);
4086
+ const header = records[0];
4087
+ if (header.id !== id) {
4088
+ throw new Error(`Session id mismatch: expected ${id}, found ${header.id}`);
3489
4089
  }
3490
- const definition = {
3491
- id: registration.id,
3492
- name: registration.name,
3493
- description: registration.description,
3494
- run: registration.run,
3495
- ...registration.model ? { model: registration.model } : {}
3496
- };
3497
- const registered = {
3498
- definition,
3499
- source: { extensionPath: extension.path }
3500
- };
3501
- this.phases.set(registration.id, registered);
3502
- extension.phases.set(registration.id, registered);
3503
- this._phaseCache = null;
4090
+ return new _LocalJsonlSessionManager(
4091
+ sessionsDir,
4092
+ path,
4093
+ InMemorySessionManager.fromRecords(records)
4094
+ );
3504
4095
  }
3505
- registerProvider(config) {
3506
- if (this.bound) {
3507
- applyProviderRegistration(config);
3508
- } else {
3509
- this.pendingProviders.push({ kind: "register", config });
3510
- }
4096
+ static async list(sessionsDir) {
4097
+ const entries = await readdir4(sessionsDir, { withFileTypes: true }).catch((error) => {
4098
+ if (error.code === "ENOENT") {
4099
+ return [];
4100
+ }
4101
+ throw error;
4102
+ });
4103
+ const sessions = await Promise.all(
4104
+ entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map(async (entry) => {
4105
+ const path = join6(sessionsDir, entry.name);
4106
+ const records = await readRecords(path);
4107
+ return records ? summarizeSessionManagerRecords(records) : void 0;
4108
+ })
4109
+ );
4110
+ return sessions.filter((session) => Boolean(session)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
3511
4111
  }
3512
- unregisterProvider(name) {
3513
- if (this.bound) {
3514
- applyProviderUnregistration(name);
3515
- } else {
3516
- this.pendingProviders.push({ kind: "unregister", name });
4112
+ static async delete(sessionsDir, id) {
4113
+ const path = safeSessionPath(sessionsDir, id);
4114
+ if (!path) {
4115
+ return false;
3517
4116
  }
3518
- }
3519
- flushPendingProviders() {
3520
- for (const action of this.pendingProviders) {
3521
- if (action.kind === "register") {
3522
- applyProviderRegistration(action.config);
3523
- } else {
3524
- applyProviderUnregistration(action.name);
4117
+ return unlink(path).then(() => true).catch((error) => {
4118
+ if (error.code === "ENOENT") {
4119
+ return false;
3525
4120
  }
3526
- }
3527
- this.pendingProviders.length = 0;
4121
+ throw error;
4122
+ });
3528
4123
  }
3529
- getRegisteredPhase(id) {
3530
- return this.collectRegisteredPhases().get(id);
4124
+ getSessionId() {
4125
+ return this.inner.getSessionId();
3531
4126
  }
3532
- collectRegisteredPhases() {
3533
- if (this._phaseCache) return this._phaseCache;
3534
- this._phaseCache = new Map(this.phases);
3535
- return this._phaseCache;
4127
+ getSessionFile() {
4128
+ return this.filePath;
3536
4129
  }
3537
- };
3538
- function createExtensionRunner(options) {
3539
- return new ExtensionRunner(options);
3540
- }
3541
-
3542
- // src/extensions/loader.ts
3543
- import { existsSync as existsSync2, readFileSync } from "fs";
3544
- import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "fs/promises";
3545
- import { fileURLToPath } from "url";
3546
- import { dirname as dirname3, extname, join as join3, resolve as resolve3 } from "path";
3547
- import { createJiti } from "jiti";
3548
- var ROWAN_DIR = ".rowan";
3549
- var EXTENSIONS_DIR = "extensions";
3550
- function isExtensionFile(path) {
3551
- const extension = extname(path).toLowerCase();
3552
- return extension === ".ts" || extension === ".js";
3553
- }
3554
- function isSyntheticPath(path) {
3555
- return path.startsWith("<") && path.endsWith(">");
3556
- }
3557
- function readManifestSync(dir) {
3558
- const manifestPath = join3(dir, "package.json");
3559
- if (!existsSync2(manifestPath)) {
3560
- return void 0;
4130
+ async getHeader() {
4131
+ return this.inner.getHeader();
3561
4132
  }
3562
- try {
3563
- const content = readFileSync(manifestPath, "utf8");
3564
- const pkg = JSON.parse(content);
3565
- const rowan = pkg.rowan;
3566
- if (!rowan) return void 0;
3567
- return {
3568
- entry: rowan.extensions?.[0],
3569
- phase: rowan.phase
3570
- };
3571
- } catch {
3572
- return void 0;
4133
+ async appendMessage(message) {
4134
+ return this.appendThroughInner(() => this.inner.appendMessage(message));
3573
4135
  }
3574
- }
3575
- function jitiAliases() {
3576
- const moduleUrl = import.meta.url;
3577
- if (!moduleUrl) {
3578
- return {};
4136
+ async appendOutcome(outcome) {
4137
+ return this.appendThroughInner(() => this.inner.appendOutcome(outcome));
3579
4138
  }
3580
- const agentSourcePath = fileURLToPath(new URL("../index.ts", moduleUrl));
3581
- if (!existsSync2(agentSourcePath)) {
3582
- return {};
4139
+ async appendExecutionTurn(turn) {
4140
+ return this.appendThroughInner(() => this.inner.appendExecutionTurn(turn));
3583
4141
  }
3584
- return { "@rowan-agent/agent": agentSourcePath };
3585
- }
3586
- var sharedJiti;
3587
- function getJiti() {
3588
- sharedJiti ??= createJiti(import.meta.url || process.cwd(), {
3589
- moduleCache: false,
3590
- alias: jitiAliases()
3591
- });
3592
- return sharedJiti;
3593
- }
3594
- async function loadExtensionModule(extensionPath) {
3595
- const jiti = getJiti();
3596
- const module = await jiti.import(extensionPath, { default: true });
3597
- return typeof module === "function" ? module : void 0;
3598
- }
3599
- async function readPackageManifest(path) {
3600
- if (!existsSync2(path)) {
3601
- return void 0;
4142
+ async appendCompaction(input) {
4143
+ return this.appendThroughInner(() => this.inner.appendCompaction(input));
3602
4144
  }
3603
- const content = await readFile3(path, "utf8");
3604
- return JSON.parse(content);
3605
- }
3606
- async function resolveExtensionEntries(dir) {
3607
- const manifest = await readPackageManifest(join3(dir, "package.json"));
3608
- const declared = manifest?.rowan?.extensions;
3609
- if (declared && declared.length > 0) {
3610
- return declared.map((entry) => resolve3(dir, entry));
4145
+ async appendBranchSummary(input) {
4146
+ return this.appendThroughInner(() => this.inner.appendBranchSummary(input));
3611
4147
  }
3612
- for (const name of ["index.ts", "index.js"]) {
3613
- const entry = join3(dir, name);
3614
- if (existsSync2(entry)) {
3615
- return [entry];
4148
+ async appendSessionInfo(input) {
4149
+ return this.appendThroughInner(() => this.inner.appendSessionInfo(input));
4150
+ }
4151
+ async appendCustom(input) {
4152
+ return this.appendThroughInner(() => this.inner.appendCustom(input));
4153
+ }
4154
+ async appendSessionState(state) {
4155
+ return this.appendThroughInner(() => this.inner.appendSessionState(state));
4156
+ }
4157
+ async appendModelTranscript(transcript, meta) {
4158
+ return this.appendThroughInner(() => this.inner.appendModelTranscript(transcript, meta));
4159
+ }
4160
+ async getSessionState() {
4161
+ return this.inner.getSessionState();
4162
+ }
4163
+ async branch(entryId) {
4164
+ await this.inner.branch(entryId);
4165
+ if (entryId) {
4166
+ await this.appendThroughInner(() => this.inner.appendBranchSummary({
4167
+ fromId: entryId,
4168
+ summary: `Selected branch at ${entryId}`
4169
+ }));
3616
4170
  }
3617
4171
  }
3618
- return void 0;
3619
- }
3620
- async function discoverExtensionsInDir(dir) {
3621
- const entries = await readdir3(dir, { withFileTypes: true }).catch((error) => {
3622
- if (error.code === "ENOENT") {
3623
- return [];
4172
+ async buildAgentContext(input) {
4173
+ return this.inner.buildAgentContext(input);
4174
+ }
4175
+ async listEntries() {
4176
+ return this.inner.listEntries();
4177
+ }
4178
+ async loadExecutionTurns(filter) {
4179
+ return this.inner.loadExecutionTurns(filter);
4180
+ }
4181
+ async appendThroughInner(append) {
4182
+ const before = await this.inner.listEntries();
4183
+ const entryId = await append();
4184
+ const after = await this.inner.listEntries();
4185
+ const entry = after.find((candidate) => candidate.id === entryId);
4186
+ if (!entry || before.some((candidate) => candidate.id === entry.id)) {
4187
+ throw new Error(`Session entry was not appended: ${entryId}`);
3624
4188
  }
3625
- throw error;
3626
- });
3627
- const paths = [];
3628
- for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
3629
- const entryPath = join3(dir, entry.name);
3630
- if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
3631
- paths.push(entryPath);
3632
- continue;
4189
+ await mkdir2(this.sessionsDir, { recursive: true });
4190
+ await appendRecord(this.filePath, entry);
4191
+ return entryId;
4192
+ }
4193
+ };
4194
+
4195
+ // src/event-stream.ts
4196
+ var EventStream = class {
4197
+ queue = [];
4198
+ waiting = [];
4199
+ done = false;
4200
+ finalResultPromise;
4201
+ resolveFinalResult;
4202
+ isComplete;
4203
+ extractResult;
4204
+ constructor(isComplete, extractResult) {
4205
+ this.isComplete = isComplete;
4206
+ this.extractResult = extractResult;
4207
+ this.finalResultPromise = new Promise((resolve7) => {
4208
+ this.resolveFinalResult = resolve7;
4209
+ });
4210
+ }
4211
+ push(event) {
4212
+ if (this.done) return;
4213
+ if (this.isComplete(event)) {
4214
+ this.done = true;
4215
+ this.resolveFinalResult(this.extractResult(event));
3633
4216
  }
3634
- if (entry.isDirectory() || entry.isSymbolicLink()) {
3635
- const info = entry.isSymbolicLink() ? await stat3(entryPath).catch(() => void 0) : void 0;
3636
- if (entry.isDirectory() || info?.isDirectory()) {
3637
- const resolvedEntries = await resolveExtensionEntries(entryPath);
3638
- if (resolvedEntries) {
3639
- paths.push(...resolvedEntries);
3640
- }
3641
- }
4217
+ const waiter = this.waiting.shift();
4218
+ if (waiter) {
4219
+ waiter({ value: event, done: false });
4220
+ } else {
4221
+ this.queue.push(event);
3642
4222
  }
3643
4223
  }
3644
- return paths;
3645
- }
3646
- function loadExtensionFromFactory(factory, cwd, extensionPath = "<inline>") {
3647
- const resolvedCwd = resolve3(cwd);
3648
- const resolvedPath = isSyntheticPath(extensionPath) ? extensionPath : resolve3(resolvedCwd, extensionPath);
3649
- const name = isSyntheticPath(extensionPath) ? extensionPath : dirname3(resolvedPath).split("/").pop() ?? "unknown";
3650
- const manifestDir = isSyntheticPath(extensionPath) ? resolvedCwd : dirname3(resolvedPath);
3651
- const manifest = readManifestSync(manifestDir);
3652
- return {
3653
- path: extensionPath,
3654
- resolvedPath,
3655
- name,
3656
- factory,
3657
- manifest
3658
- };
3659
- }
3660
- async function loadExtensions(paths, cwd) {
3661
- const extensions = [];
3662
- const errors = [];
3663
- const resolvedCwd = resolve3(cwd);
3664
- for (const path of paths) {
3665
- const resolvedPath = resolve3(resolvedCwd, path);
3666
- try {
3667
- const factory = await loadExtensionModule(resolvedPath);
3668
- if (!factory) {
3669
- errors.push({
3670
- path: resolvedPath,
3671
- error: `Extension does not export a valid factory function: ${path}`
3672
- });
3673
- continue;
4224
+ end(result) {
4225
+ this.done = true;
4226
+ if (result !== void 0) {
4227
+ this.resolveFinalResult(result);
4228
+ }
4229
+ while (this.waiting.length > 0) {
4230
+ const waiter = this.waiting.shift();
4231
+ waiter({ value: void 0, done: true });
4232
+ }
4233
+ }
4234
+ async *[Symbol.asyncIterator]() {
4235
+ while (true) {
4236
+ if (this.queue.length > 0) {
4237
+ yield this.queue.shift();
4238
+ } else if (this.done) {
4239
+ return;
4240
+ } else {
4241
+ const result = await new Promise((resolve7) => this.waiting.push(resolve7));
4242
+ if (result.done) return;
4243
+ yield result.value;
3674
4244
  }
3675
- extensions.push(loadExtensionFromFactory(factory, resolvedCwd, resolvedPath));
3676
- } catch (error) {
3677
- errors.push({
3678
- path: resolvedPath,
3679
- error: error instanceof Error ? error.message : String(error)
3680
- });
3681
4245
  }
3682
4246
  }
3683
- return { extensions, errors };
3684
- }
3685
- async function discoverAndLoadExtensions(cwd) {
3686
- const extensionsDir = join3(resolve3(cwd), ROWAN_DIR, EXTENSIONS_DIR);
3687
- const paths = await discoverExtensionsInDir(extensionsDir);
3688
- return loadExtensions(paths, cwd);
3689
- }
4247
+ result() {
4248
+ return this.finalResultPromise;
4249
+ }
4250
+ };
4251
+ var AgentEventStream = class extends EventStream {
4252
+ constructor() {
4253
+ super(
4254
+ (event) => event.type === "agent_end",
4255
+ (event) => event.type === "agent_end" ? event.messages : []
4256
+ );
4257
+ }
4258
+ };
3690
4259
 
3691
4260
  // src/index.ts
3692
4261
  import {
@@ -3696,10 +4265,12 @@ import {
3696
4265
  export {
3697
4266
  Agent,
3698
4267
  AgentEventStream,
4268
+ EmptyResponseError,
3699
4269
  EventStream,
3700
4270
  ExtensionRunner,
3701
4271
  HooksManager,
3702
4272
  LocalJsonlSessionManager,
4273
+ MissingRouteToolCallError,
3703
4274
  appendUserTurn,
3704
4275
  buildModelRequest,
3705
4276
  buildSystemPrompt,
@@ -3716,24 +4287,20 @@ export {
3716
4287
  createSession,
3717
4288
  createSourceInfo,
3718
4289
  createTimestamp,
3719
- discoverAndLoadExtensions,
3720
4290
  getGlobalHooks,
3721
4291
  interpolateEnvVars,
3722
4292
  latestUserInput,
3723
4293
  loadConfigFile,
3724
- loadExtensionFromFactory,
3725
- loadExtensions,
4294
+ loadPhase,
3726
4295
  loadPhases,
3727
- loadSkill,
3728
- loadSkills,
3729
4296
  messageContentText,
3730
4297
  parseModelRef,
3731
4298
  registerBuiltInApiProviders,
3732
4299
  registerConfigModels,
4300
+ reloadPhases,
3733
4301
  resetGlobalHooks,
3734
4302
  resolveDefaultModel,
3735
4303
  resolveInWorkspace,
3736
- resolveSkillPath,
3737
4304
  resolveWorkspacePaths,
3738
4305
  serializeSkills
3739
4306
  };