@rowan-agent/agent 0.4.10 → 0.5.0

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