operon-agents 0.1.0-alpha.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/LICENSE +55 -0
- package/README.md +18 -0
- package/dist/app-server/client.d.ts +80 -0
- package/dist/app-server/client.js +187 -0
- package/dist/app-server/codec.d.ts +52 -0
- package/dist/app-server/codec.js +252 -0
- package/dist/app-server/index.d.ts +22 -0
- package/dist/app-server/index.js +22 -0
- package/dist/app-server/main.d.ts +3 -0
- package/dist/app-server/main.js +85 -0
- package/dist/app-server/protocol.d.ts +217 -0
- package/dist/app-server/protocol.js +100 -0
- package/dist/app-server/server.d.ts +51 -0
- package/dist/app-server/server.js +274 -0
- package/dist/core.d.ts +12 -0
- package/dist/core.js +12 -0
- package/dist/cron/clock.d.ts +11 -0
- package/dist/cron/clock.js +19 -0
- package/dist/cron/cron-expr.d.ts +15 -0
- package/dist/cron/cron-expr.js +267 -0
- package/dist/cron/extension.d.ts +25 -0
- package/dist/cron/extension.js +141 -0
- package/dist/cron/index.d.ts +20 -0
- package/dist/cron/index.js +11 -0
- package/dist/cron/jitter.d.ts +18 -0
- package/dist/cron/jitter.js +46 -0
- package/dist/cron/manager.d.ts +55 -0
- package/dist/cron/manager.js +123 -0
- package/dist/cron/persist.d.ts +10 -0
- package/dist/cron/persist.js +22 -0
- package/dist/cron/scheduler.d.ts +25 -0
- package/dist/cron/scheduler.js +192 -0
- package/dist/cron/session-store.d.ts +14 -0
- package/dist/cron/session-store.js +50 -0
- package/dist/cron/time-format.d.ts +2 -0
- package/dist/cron/time-format.js +12 -0
- package/dist/cron/tools.d.ts +7 -0
- package/dist/cron/tools.js +161 -0
- package/dist/cron/types.d.ts +9 -0
- package/dist/cron/types.js +2 -0
- package/dist/extensions/index.d.ts +23 -0
- package/dist/extensions/index.js +41 -0
- package/dist/extensions/loader.d.ts +121 -0
- package/dist/extensions/loader.js +285 -0
- package/dist/extensions/manager.d.ts +103 -0
- package/dist/extensions/manager.js +190 -0
- package/dist/extensions/runtime.d.ts +188 -0
- package/dist/extensions/runtime.js +1127 -0
- package/dist/extensions/services.d.ts +85 -0
- package/dist/extensions/services.js +265 -0
- package/dist/extensions/types.d.ts +594 -0
- package/dist/extensions/types.js +2 -0
- package/dist/harness.d.ts +746 -0
- package/dist/harness.js +1483 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +29 -0
- package/dist/internal.d.ts +9 -0
- package/dist/internal.js +9 -0
- package/dist/llm.d.ts +2 -0
- package/dist/llm.js +2 -0
- package/dist/local.d.ts +24 -0
- package/dist/local.js +58 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +2 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +2 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2 -0
- package/dist/tracing.d.ts +2 -0
- package/dist/tracing.js +2 -0
- package/package.json +78 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CronTask } from "./types.ts";
|
|
2
|
+
export type SessionCronTaskInit = Omit<CronTask, "id" | "createdAt">;
|
|
3
|
+
export declare class SessionCronStore {
|
|
4
|
+
private readonly tasks;
|
|
5
|
+
add(init: SessionCronTaskInit, nowMs: number): CronTask;
|
|
6
|
+
adopt(task: CronTask): void;
|
|
7
|
+
markFired(id: string, lastFiredAt: number): CronTask | undefined;
|
|
8
|
+
get(id: string): CronTask | undefined;
|
|
9
|
+
list(): readonly CronTask[];
|
|
10
|
+
remove(ids: readonly string[]): readonly string[];
|
|
11
|
+
clear(): void;
|
|
12
|
+
private generateUniqueId;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=session-store.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
const ID_REGEX = /^[0-9a-f]{8}$/;
|
|
3
|
+
const MAX_ID_ATTEMPTS = 8;
|
|
4
|
+
export class SessionCronStore {
|
|
5
|
+
tasks = new Map();
|
|
6
|
+
add(init, nowMs) {
|
|
7
|
+
const id = this.generateUniqueId();
|
|
8
|
+
const task = { ...init, id, createdAt: nowMs };
|
|
9
|
+
this.tasks.set(id, task);
|
|
10
|
+
return task;
|
|
11
|
+
}
|
|
12
|
+
adopt(task) {
|
|
13
|
+
this.tasks.set(task.id, task);
|
|
14
|
+
}
|
|
15
|
+
markFired(id, lastFiredAt) {
|
|
16
|
+
const existing = this.tasks.get(id);
|
|
17
|
+
if (existing === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
const updated = { ...existing, lastFiredAt };
|
|
20
|
+
this.tasks.set(id, updated);
|
|
21
|
+
return updated;
|
|
22
|
+
}
|
|
23
|
+
get(id) {
|
|
24
|
+
return this.tasks.get(id);
|
|
25
|
+
}
|
|
26
|
+
list() {
|
|
27
|
+
return Array.from(this.tasks.values());
|
|
28
|
+
}
|
|
29
|
+
remove(ids) {
|
|
30
|
+
const removed = [];
|
|
31
|
+
for (const id of ids)
|
|
32
|
+
if (this.tasks.delete(id))
|
|
33
|
+
removed.push(id);
|
|
34
|
+
return removed;
|
|
35
|
+
}
|
|
36
|
+
clear() {
|
|
37
|
+
this.tasks.clear();
|
|
38
|
+
}
|
|
39
|
+
generateUniqueId() {
|
|
40
|
+
for (let attempt = 0; attempt < MAX_ID_ATTEMPTS; attempt++) {
|
|
41
|
+
const candidate = randomBytes(4).toString("hex");
|
|
42
|
+
if (!ID_REGEX.test(candidate))
|
|
43
|
+
continue;
|
|
44
|
+
if (!this.tasks.has(candidate))
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`SessionCronStore: failed to generate a unique 8-hex id after ${MAX_ID_ATTEMPTS} attempts`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=session-store.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function formatLocalIsoWithOffset(ms) {
|
|
2
|
+
const date = new Date(ms);
|
|
3
|
+
const offsetMin = -date.getTimezoneOffset();
|
|
4
|
+
const sign = offsetMin >= 0 ? "+" : "-";
|
|
5
|
+
const absOffset = Math.abs(offsetMin);
|
|
6
|
+
const offset = `${sign}${pad(Math.floor(absOffset / 60))}:${pad(absOffset % 60)}`;
|
|
7
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${String(date.getMilliseconds()).padStart(3, "0")}${offset}`;
|
|
8
|
+
}
|
|
9
|
+
function pad(n) {
|
|
10
|
+
return String(n).padStart(2, "0");
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=time-format.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Tool } from "operon-agents-core";
|
|
2
|
+
import type { CronManager } from "./manager.ts";
|
|
3
|
+
export declare const MAX_CRON_JOBS_PER_SESSION = 50;
|
|
4
|
+
export declare function cronCreateTool(manager: CronManager): Tool;
|
|
5
|
+
export declare function cronDeleteTool(manager: CronManager): Tool;
|
|
6
|
+
export declare function cronListTool(manager: CronManager): Tool;
|
|
7
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ToolAccesses, defineTool, globApproval } from "operon-agents-core";
|
|
3
|
+
import { computeNextCronRun, cronToHuman, hasFireWithinYears, parseCronExpression, } from "./cron-expr.js";
|
|
4
|
+
import { jitteredNextCronRunMs, oneShotJitteredNextCronRunMs } from "./jitter.js";
|
|
5
|
+
import { formatLocalIsoWithOffset } from "./time-format.js";
|
|
6
|
+
export const MAX_CRON_JOBS_PER_SESSION = 50;
|
|
7
|
+
const MAX_PROMPT_BYTES = 8 * 1024;
|
|
8
|
+
const ONE_SHOT_MAX_FUTURE_MS = 350 * 24 * 60 * 60 * 1000;
|
|
9
|
+
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
|
10
|
+
const PROMPT_PREVIEW_BYTES = 200;
|
|
11
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
12
|
+
function text(s, isError = false) {
|
|
13
|
+
return { content: [{ type: "text", text: s }], isError };
|
|
14
|
+
}
|
|
15
|
+
export function cronCreateTool(manager) {
|
|
16
|
+
return defineTool({
|
|
17
|
+
name: "CronCreate",
|
|
18
|
+
description: 'Schedule a prompt to be re-injected into this session at a future time. `cron` is a 5-field local-time expression ("M H DoM Mon DoW"). recurring=true (default) fires on every match until deleted or auto-expired after 7 days; recurring=false fires once then auto-deletes.',
|
|
19
|
+
params: z.object({
|
|
20
|
+
cron: z.string().describe('5-field cron expression in local time, e.g. "*/5 * * * *" (every 5 min) or "30 14 28 2 *".'),
|
|
21
|
+
prompt: z.string().min(1).max(MAX_PROMPT_BYTES).describe("The prompt to enqueue at each fire time."),
|
|
22
|
+
recurring: z.boolean().optional().default(true).describe("true = repeat until deleted/expired; false = fire once."),
|
|
23
|
+
}),
|
|
24
|
+
resolve: (args) => {
|
|
25
|
+
const normalizedCron = args.cron.trim().split(/\s+/).join(" ");
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = parseCronExpression(normalizedCron);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
return errorPlan("CronCreate", `Invalid cron expression: ${err instanceof Error ? err.message : String(err)}`);
|
|
32
|
+
}
|
|
33
|
+
const nowAtPrepare = manager.clocks.wallNow();
|
|
34
|
+
if (!hasFireWithinYears(parsed, 5, nowAtPrepare)) {
|
|
35
|
+
return errorPlan("CronCreate", `Cron expression ${JSON.stringify(normalizedCron)} has no fire within 5 years; refusing to schedule.`);
|
|
36
|
+
}
|
|
37
|
+
if (manager.store.list().length >= MAX_CRON_JOBS_PER_SESSION) {
|
|
38
|
+
return errorPlan("CronCreate", `Cron job cap reached (max ${MAX_CRON_JOBS_PER_SESSION} per session).`);
|
|
39
|
+
}
|
|
40
|
+
const byteLen = Buffer.byteLength(args.prompt, "utf8");
|
|
41
|
+
if (byteLen > MAX_PROMPT_BYTES) {
|
|
42
|
+
return errorPlan("CronCreate", `Prompt exceeds ${MAX_PROMPT_BYTES} bytes (got ${byteLen}).`);
|
|
43
|
+
}
|
|
44
|
+
const recurring = args.recurring !== false;
|
|
45
|
+
if (!recurring) {
|
|
46
|
+
const firstFire = computeNextCronRun(parsed, nowAtPrepare);
|
|
47
|
+
if (firstFire !== null && firstFire - nowAtPrepare > ONE_SHOT_MAX_FUTURE_MS) {
|
|
48
|
+
return errorPlan("CronCreate", `One-shot cron ${JSON.stringify(normalizedCron)} would not fire until ${formatLocalIsoWithOffset(firstFire)} (more than a year out). The pinned day/month has already passed this year — pick a future date or use wildcards.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
...globApproval("CronCreate", JSON.stringify({ cron: normalizedCron, prompt: args.prompt, recurring })),
|
|
53
|
+
accesses: ToolAccesses.none(),
|
|
54
|
+
display: { title: recurring ? `Schedule cron ${normalizedCron}` : `Schedule one-shot ${normalizedCron}` },
|
|
55
|
+
run: async () => {
|
|
56
|
+
const nowMs = manager.clocks.wallNow();
|
|
57
|
+
if (manager.store.list().length >= MAX_CRON_JOBS_PER_SESSION) {
|
|
58
|
+
return text(`Cron job cap reached (max ${MAX_CRON_JOBS_PER_SESSION} per session).`, true);
|
|
59
|
+
}
|
|
60
|
+
const task = manager.addTask({ cron: normalizedCron, prompt: args.prompt, recurring });
|
|
61
|
+
const ideal = computeNextCronRun(parsed, nowMs);
|
|
62
|
+
const nextFireAt = ideal === null ? null : recurring ? jitteredNextCronRunMs(task, parsed, ideal) : oneShotJitteredNextCronRunMs(task, ideal);
|
|
63
|
+
return text([
|
|
64
|
+
`id: ${task.id}`,
|
|
65
|
+
`cron: ${normalizedCron}`,
|
|
66
|
+
`humanSchedule: ${cronToHuman(parsed)}`,
|
|
67
|
+
`recurring: ${String(recurring)}`,
|
|
68
|
+
`nextFireAt: ${nextFireAt === null ? "null" : formatLocalIsoWithOffset(nextFireAt)}`,
|
|
69
|
+
].join("\n"));
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function cronDeleteTool(manager) {
|
|
76
|
+
return defineTool({
|
|
77
|
+
name: "CronDelete",
|
|
78
|
+
description: "Cancel a scheduled cron job by its 8-hex id (from CronCreate / CronList).",
|
|
79
|
+
params: z.object({ id: z.string().describe("The 8-hex cron job id.") }),
|
|
80
|
+
resolve: (args) => {
|
|
81
|
+
if (!ID_PATTERN.test(args.id)) {
|
|
82
|
+
return errorPlan("CronDelete", `Invalid cron job id ${JSON.stringify(args.id)} — must be 8 lowercase hex characters.`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
...globApproval("CronDelete", args.id),
|
|
86
|
+
accesses: ToolAccesses.none(),
|
|
87
|
+
display: { title: `Delete cron ${args.id}` },
|
|
88
|
+
run: async () => {
|
|
89
|
+
const removed = manager.removeTasks([args.id]);
|
|
90
|
+
if (removed.length === 0)
|
|
91
|
+
return text(`No cron job with id ${args.id}.`, true);
|
|
92
|
+
return text(`Deleted cron job ${args.id}.`);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function previewPrompt(prompt) {
|
|
99
|
+
const buf = Buffer.from(prompt, "utf8");
|
|
100
|
+
if (buf.byteLength <= PROMPT_PREVIEW_BYTES)
|
|
101
|
+
return prompt;
|
|
102
|
+
let end = PROMPT_PREVIEW_BYTES;
|
|
103
|
+
while (end > 0 && (buf[end] & 0b1100_0000) === 0b1000_0000)
|
|
104
|
+
end--;
|
|
105
|
+
return `${buf.subarray(0, end).toString("utf8")}…(truncated)`;
|
|
106
|
+
}
|
|
107
|
+
export function cronListTool(manager) {
|
|
108
|
+
return defineTool({
|
|
109
|
+
name: "CronList",
|
|
110
|
+
description: "List the cron jobs scheduled in this session (id, expression, human schedule, next fire, recurring, age, stale).",
|
|
111
|
+
params: z.object({}),
|
|
112
|
+
resolve: () => ({
|
|
113
|
+
...globApproval("CronList", "list"),
|
|
114
|
+
accesses: ToolAccesses.none(),
|
|
115
|
+
display: { title: "List cron jobs" },
|
|
116
|
+
run: async () => {
|
|
117
|
+
const tasks = manager.store.list();
|
|
118
|
+
const nowMs = manager.clocks.wallNow();
|
|
119
|
+
const header = `cron_jobs: ${tasks.length}`;
|
|
120
|
+
if (tasks.length === 0)
|
|
121
|
+
return text(`${header}\nNo cron jobs scheduled.`);
|
|
122
|
+
return text(`${header}\n${tasks.map((t) => renderRecord(manager, t, nowMs)).join("\n---\n")}`);
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function renderRecord(manager, task, nowMs) {
|
|
128
|
+
const recurring = task.recurring !== false;
|
|
129
|
+
const ageMs = nowMs - task.createdAt;
|
|
130
|
+
const ageDays = Number.isFinite(ageMs) ? ageMs / MS_PER_DAY : 0;
|
|
131
|
+
const stale = manager.isStale(task);
|
|
132
|
+
let humanSchedule = task.cron;
|
|
133
|
+
let nextFireAtIso = "null";
|
|
134
|
+
try {
|
|
135
|
+
humanSchedule = cronToHuman(parseCronExpression(task.cron));
|
|
136
|
+
const nextFireMs = manager.getNextFireForTask(task.id);
|
|
137
|
+
if (nextFireMs !== null)
|
|
138
|
+
nextFireAtIso = formatLocalIsoWithOffset(nextFireMs);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* malformed cron — leave raw + null */
|
|
142
|
+
}
|
|
143
|
+
return [
|
|
144
|
+
`id: ${task.id}`,
|
|
145
|
+
`cron: ${task.cron}`,
|
|
146
|
+
`humanSchedule: ${humanSchedule}`,
|
|
147
|
+
`prompt: ${JSON.stringify(previewPrompt(task.prompt))}`,
|
|
148
|
+
`nextFireAt: ${nextFireAtIso}`,
|
|
149
|
+
`recurring: ${String(recurring)}`,
|
|
150
|
+
`ageDays: ${ageDays.toFixed(2)}`,
|
|
151
|
+
`stale: ${String(stale)}`,
|
|
152
|
+
].join("\n");
|
|
153
|
+
}
|
|
154
|
+
function errorPlan(tool, message) {
|
|
155
|
+
return {
|
|
156
|
+
approvalRule: tool,
|
|
157
|
+
accesses: ToolAccesses.none(),
|
|
158
|
+
run: async () => text(message, true),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Capability } from "operon-agents-core";
|
|
2
|
+
import type { ExtensionDefinition, ExtensionHost } from "./types.ts";
|
|
3
|
+
export { ExtensionRuntime } from "./runtime.ts";
|
|
4
|
+
export { ExtensionLoader, createExtensionLoader } from "./loader.ts";
|
|
5
|
+
export { ServiceRegistry, ServiceUnavailableError, deadServiceHandle } from "./services.ts";
|
|
6
|
+
export { HarnessExtensionManager, stageDefinition } from "./manager.ts";
|
|
7
|
+
export type { ExtensionHostBridge, StagingOptions, HeldSession, StagedDefinition } from "./manager.ts";
|
|
8
|
+
export type { ServiceOptions, ServiceUnavailableReason } from "./services.ts";
|
|
9
|
+
export type { ExtensionManifest, ExtensionFileState, ExtensionFileStatus, ExtensionAttachTarget } from "./loader.ts";
|
|
10
|
+
export type { ExtensionActions, ExtensionAPI, ExtensionCommand, ExtensionCommandResult, ExtensionRecordEntry, ExtensionSteerOptions, ExtensionDefinition, ExtensionHostContext, ExtensionEventContext, ExtensionEventMap, ExtensionEventName, ExtensionHandler, ExtensionHost, ExtensionModelRequestEvent, ExtensionModelRequestResult, ExtensionModelResponseEvent, ExtensionResultMap, ExtensionRunSettledEvent, ExtensionRunSettledResult, ExtensionRunStartEvent, ExtensionRunStartResult, ExtensionSessionContext, ExtensionSetupContext, ExtensionSessionEndEvent, ExtensionSessionStartEvent, ExtensionState, ExtensionToolSpec, ExtensionStepEndEvent, ExtensionStepEndResult, ExtensionStepStartEvent, ExtensionStepStartResult, ExtensionToolAuthorizeEvent, ExtensionToolAuthorizeResult, ExtensionToolCallEvent, ExtensionToolCallResult, ExtensionToolResultEvent, ExtensionCompactionBeforeEvent, ExtensionCompactionBeforeResult, ExtensionProviderHeadersEvent, ExtensionProviderHeadersResult, ExtensionProviderPayloadEvent, ExtensionProviderPayloadResult, ExtensionProviderResponseEvent, ProviderHeaders, HarnessSessionHandle, ModelProvider, SessionEndReason, SessionStartReason, } from "./types.ts";
|
|
11
|
+
/**
|
|
12
|
+
* Build one isolated, session-scoped runtime for programmatic extensions.
|
|
13
|
+
*
|
|
14
|
+
* Every decision point in the extension contract is a `LoopHooks` slot — that is the only
|
|
15
|
+
* channel where a handler's return value can reach the loop. Observation does NOT come through
|
|
16
|
+
* here: observation goes through `api.onEvent`, which passes the session's event stream straight
|
|
17
|
+
* through (with fault isolation and teardown), rather than through this hook table.
|
|
18
|
+
*/
|
|
19
|
+
export declare function extensionsCapability(definitions: readonly ExtensionDefinition[], options?: {
|
|
20
|
+
readonly host?: ExtensionHost;
|
|
21
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
22
|
+
}): Capability;
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ExtensionRuntime } from "./runtime.js";
|
|
2
|
+
export { ExtensionRuntime } from "./runtime.js";
|
|
3
|
+
export { ExtensionLoader, createExtensionLoader } from "./loader.js";
|
|
4
|
+
export { ServiceRegistry, ServiceUnavailableError, deadServiceHandle } from "./services.js";
|
|
5
|
+
export { HarnessExtensionManager, stageDefinition } from "./manager.js";
|
|
6
|
+
/**
|
|
7
|
+
* Build one isolated, session-scoped runtime for programmatic extensions.
|
|
8
|
+
*
|
|
9
|
+
* Every decision point in the extension contract is a `LoopHooks` slot — that is the only
|
|
10
|
+
* channel where a handler's return value can reach the loop. Observation does NOT come through
|
|
11
|
+
* here: observation goes through `api.onEvent`, which passes the session's event stream straight
|
|
12
|
+
* through (with fault isolation and teardown), rather than through this hook table.
|
|
13
|
+
*/
|
|
14
|
+
export function extensionsCapability(definitions, options = {}) {
|
|
15
|
+
const runtime = new ExtensionRuntime(definitions, options.host, options.params);
|
|
16
|
+
return {
|
|
17
|
+
name: "extensions",
|
|
18
|
+
service: runtime,
|
|
19
|
+
toolProviders: [{ id: "extensions", listTools: () => runtime.listTools() }],
|
|
20
|
+
toolFilters: [runtime.filterTools],
|
|
21
|
+
gates: { compaction: runtime.compactionGate },
|
|
22
|
+
// Stable array reference — `setup` (openSession) fills it before any run assembles.
|
|
23
|
+
injectors: runtime.listInjectors(),
|
|
24
|
+
hooks: {
|
|
25
|
+
beforeRun: async (ctx) => runtime.beforeRun(ctx, ctx.input),
|
|
26
|
+
beforeStep: async (ctx) => runtime.beforeStep(ctx, ctx.context, ctx.system),
|
|
27
|
+
afterStep: async (ctx) => runtime.afterStep(ctx, ctx.context, ctx.usage, ctx.stopReason),
|
|
28
|
+
shouldContinueAfterStop: async (ctx) => runtime.runSettled(ctx, ctx.usage, ctx.stopReason),
|
|
29
|
+
beforeModelRequest: async (ctx) => runtime.beforeModelRequest(ctx, ctx.request, ctx.context),
|
|
30
|
+
afterModelResponse: async (ctx) => runtime.afterModelResponse(ctx, ctx.request, ctx.response, ctx.context),
|
|
31
|
+
prepareToolExecution: async (ctx) => runtime.beforeToolCall(ctx, ctx.toolCall.name, ctx.toolCall.id, ctx.tool, ctx.args),
|
|
32
|
+
authorizeToolExecution: async (ctx) => runtime.authorizeToolCall(ctx, ctx.toolCall.name, ctx.toolCall.id, ctx.tool, ctx.args, ctx.plan),
|
|
33
|
+
finalizeToolResult: async (ctx) => runtime.afterToolResult(ctx, ctx.toolCall.name, ctx.toolCall.id, ctx.tool, ctx.args, ctx.result),
|
|
34
|
+
},
|
|
35
|
+
openSession: (ctx) => runtime.open(ctx),
|
|
36
|
+
closeSession: () => runtime.close(),
|
|
37
|
+
start: (ctx) => runtime.attachRun(ctx),
|
|
38
|
+
stop: () => runtime.detachRun(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ExtensionDefinition } from "./types.ts";
|
|
2
|
+
/** The slice of a session the loader swaps extensions on — `HarnessSession` satisfies it. */
|
|
3
|
+
export interface ExtensionAttachTarget {
|
|
4
|
+
attachExtension(definition: ExtensionDefinition): Promise<void>;
|
|
5
|
+
detachExtension(extensionId: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* File-extension loader — the shell that turns a extension directory into extension VALUES for
|
|
9
|
+
* `attachExtension`. The framework core stays file-blind: everything here ends in a plain
|
|
10
|
+
* `ExtensionDefinition`, handed over the same channel host-authored extensions use.
|
|
11
|
+
*
|
|
12
|
+
* Layout: one folder per extension under `directory`, holding a `manifest.json` plus a bundled
|
|
13
|
+
* entry file (dependencies baked in by the author at publish time — no install step here).
|
|
14
|
+
*
|
|
15
|
+
* Trust model — loading is consent, and consent is MANUAL:
|
|
16
|
+
* - `load()` / `reload()` are explicit host actions; performing one approves the extension at
|
|
17
|
+
* its current file mtime, recorded in `.approvals.json` beside the extensions.
|
|
18
|
+
* - `loadApproved()` (startup) only honors extensions whose entry is byte-for-byte the one
|
|
19
|
+
* approved (mtime match). A changed or new extension is reported, never silently loaded —
|
|
20
|
+
* an agent with write access must not be able to plant code that runs on next open.
|
|
21
|
+
*
|
|
22
|
+
* Reload mechanics: native `import()` with a `?v=<mtime>` suffix — a changed file gets a
|
|
23
|
+
* fresh module, an unchanged one hits the cache. Old module instances stay resident until
|
|
24
|
+
* process exit; that is the accepted cost of not restarting.
|
|
25
|
+
*/
|
|
26
|
+
/** This framework's version — what a manifest's `engine` is checked against. */
|
|
27
|
+
export declare const FRAMEWORK_VERSION: string;
|
|
28
|
+
/** Semver ordering for plain `x.y.z[-pre]` strings (a prerelease sorts below its release). */
|
|
29
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
30
|
+
/**
|
|
31
|
+
* What must be known about an extension BEFORE its code is imported — find it (`id`), import
|
|
32
|
+
* it (`entry`, gated by `engine`), show it (`version`, `name`, `description`) — and nothing more. What the
|
|
33
|
+
* extension provides (`create`) and consumes (`uses`) lives on the definition it exports, the
|
|
34
|
+
* same on both channels. Only `id` is validated; unknown fields pass through untouched, so a
|
|
35
|
+
* manifest written for a newer loader still loads here.
|
|
36
|
+
*/
|
|
37
|
+
export interface ExtensionManifest {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly version?: string;
|
|
40
|
+
/** Entry file relative to the extension folder. Default: "index.js". */
|
|
41
|
+
readonly entry?: string;
|
|
42
|
+
/** The lowest framework version this bundle was built for (`x.y.z[-pre]`). Checked BEFORE the
|
|
43
|
+
* import: an older host lists the extension as `error` and refuses to load it, instead of
|
|
44
|
+
* importing code written against an API it does not have. */
|
|
45
|
+
readonly engine?: string;
|
|
46
|
+
/** Display name for listings (falls back to `id` in UIs). */
|
|
47
|
+
readonly name?: string;
|
|
48
|
+
/** One line for listings. */
|
|
49
|
+
readonly description?: string;
|
|
50
|
+
}
|
|
51
|
+
export type ExtensionFileState =
|
|
52
|
+
/** Approved at the current mtime and produced a definition this loader holds. */
|
|
53
|
+
"loaded"
|
|
54
|
+
/** Approved at the current mtime but not imported yet (call `loadApproved` or `load`). */
|
|
55
|
+
| "approved"
|
|
56
|
+
/** Never approved. Loads only through an explicit `load()`. */
|
|
57
|
+
| "new"
|
|
58
|
+
/** Approved before, but the entry file changed since. Loads only through `reload()`. */
|
|
59
|
+
| "changed"
|
|
60
|
+
/** Folder present but unusable (bad manifest, missing entry). */
|
|
61
|
+
| "error";
|
|
62
|
+
export interface ExtensionFileStatus {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly dir: string;
|
|
65
|
+
readonly state: ExtensionFileState;
|
|
66
|
+
readonly version?: string;
|
|
67
|
+
readonly engine?: string;
|
|
68
|
+
readonly name?: string;
|
|
69
|
+
readonly description?: string;
|
|
70
|
+
readonly error?: string;
|
|
71
|
+
}
|
|
72
|
+
export declare class ExtensionLoader {
|
|
73
|
+
private readonly directory;
|
|
74
|
+
private readonly approvalsPath;
|
|
75
|
+
/** Live definitions by extension id — the values a host hands to `attachExtension`. */
|
|
76
|
+
private readonly loaded;
|
|
77
|
+
constructor(options: {
|
|
78
|
+
readonly directory: string;
|
|
79
|
+
});
|
|
80
|
+
/** The parsed manifest of one extension, by id. */
|
|
81
|
+
manifest(id: string): Promise<ExtensionManifest>;
|
|
82
|
+
/** The folder a extension lives in — where an extension keeps its data files. */
|
|
83
|
+
manifestDir(id: string): Promise<string>;
|
|
84
|
+
/** Every extension folder under the directory, with its trust/load state. */
|
|
85
|
+
list(): Promise<ExtensionFileStatus[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Import one extension and approve it as it stands. The call itself is the consent — reserve
|
|
88
|
+
* it for user-initiated actions, never for something an agent triggers on its own.
|
|
89
|
+
*/
|
|
90
|
+
load(id: string): Promise<ExtensionDefinition>;
|
|
91
|
+
/** Re-import after an edit and roll the approval forward to the file as it is now. */
|
|
92
|
+
reload(id: string): Promise<ExtensionDefinition>;
|
|
93
|
+
/** Forget a extension: drop the held definition and revoke its approval. */
|
|
94
|
+
unload(id: string): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Startup path: import every extension still byte-identical to what was approved. Changed and
|
|
97
|
+
* new extensions are skipped and reported so the host can surface them for a manual decision.
|
|
98
|
+
*/
|
|
99
|
+
loadApproved(): Promise<{
|
|
100
|
+
loaded: ExtensionDefinition[];
|
|
101
|
+
skipped: ExtensionFileStatus[];
|
|
102
|
+
}>;
|
|
103
|
+
/** The definitions currently held. On the standalone-loader path these reach sessions through
|
|
104
|
+
* `attachExtension`/`reloadInto`; a harness built with `extensionDir` registers them itself
|
|
105
|
+
* (`harness.extensions.load`), which is the path that also runs `create` halves. */
|
|
106
|
+
definitions(): ExtensionDefinition[];
|
|
107
|
+
/** Convenience: reload a extension and swap it on a live session (detach old, attach new). */
|
|
108
|
+
reloadInto(session: ExtensionAttachTarget, id: string): Promise<ExtensionDefinition>;
|
|
109
|
+
private readExtensionDirs;
|
|
110
|
+
private scan;
|
|
111
|
+
private find;
|
|
112
|
+
private stateOf;
|
|
113
|
+
private importDefinition;
|
|
114
|
+
private approve;
|
|
115
|
+
private readApprovals;
|
|
116
|
+
private writeApprovals;
|
|
117
|
+
}
|
|
118
|
+
export declare function createExtensionLoader(options: {
|
|
119
|
+
readonly directory: string;
|
|
120
|
+
}): ExtensionLoader;
|
|
121
|
+
//# sourceMappingURL=loader.d.ts.map
|