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