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