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.
Files changed (71) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +18 -0
  3. package/dist/app-server/client.d.ts +80 -0
  4. package/dist/app-server/client.js +187 -0
  5. package/dist/app-server/codec.d.ts +52 -0
  6. package/dist/app-server/codec.js +252 -0
  7. package/dist/app-server/index.d.ts +22 -0
  8. package/dist/app-server/index.js +22 -0
  9. package/dist/app-server/main.d.ts +3 -0
  10. package/dist/app-server/main.js +85 -0
  11. package/dist/app-server/protocol.d.ts +217 -0
  12. package/dist/app-server/protocol.js +100 -0
  13. package/dist/app-server/server.d.ts +51 -0
  14. package/dist/app-server/server.js +274 -0
  15. package/dist/core.d.ts +12 -0
  16. package/dist/core.js +12 -0
  17. package/dist/cron/clock.d.ts +11 -0
  18. package/dist/cron/clock.js +19 -0
  19. package/dist/cron/cron-expr.d.ts +15 -0
  20. package/dist/cron/cron-expr.js +267 -0
  21. package/dist/cron/extension.d.ts +25 -0
  22. package/dist/cron/extension.js +141 -0
  23. package/dist/cron/index.d.ts +20 -0
  24. package/dist/cron/index.js +11 -0
  25. package/dist/cron/jitter.d.ts +18 -0
  26. package/dist/cron/jitter.js +46 -0
  27. package/dist/cron/manager.d.ts +55 -0
  28. package/dist/cron/manager.js +123 -0
  29. package/dist/cron/persist.d.ts +10 -0
  30. package/dist/cron/persist.js +22 -0
  31. package/dist/cron/scheduler.d.ts +25 -0
  32. package/dist/cron/scheduler.js +192 -0
  33. package/dist/cron/session-store.d.ts +14 -0
  34. package/dist/cron/session-store.js +50 -0
  35. package/dist/cron/time-format.d.ts +2 -0
  36. package/dist/cron/time-format.js +12 -0
  37. package/dist/cron/tools.d.ts +7 -0
  38. package/dist/cron/tools.js +161 -0
  39. package/dist/cron/types.d.ts +9 -0
  40. package/dist/cron/types.js +2 -0
  41. package/dist/extensions/index.d.ts +23 -0
  42. package/dist/extensions/index.js +41 -0
  43. package/dist/extensions/loader.d.ts +121 -0
  44. package/dist/extensions/loader.js +285 -0
  45. package/dist/extensions/manager.d.ts +103 -0
  46. package/dist/extensions/manager.js +190 -0
  47. package/dist/extensions/runtime.d.ts +188 -0
  48. package/dist/extensions/runtime.js +1127 -0
  49. package/dist/extensions/services.d.ts +85 -0
  50. package/dist/extensions/services.js +265 -0
  51. package/dist/extensions/types.d.ts +594 -0
  52. package/dist/extensions/types.js +2 -0
  53. package/dist/harness.d.ts +746 -0
  54. package/dist/harness.js +1483 -0
  55. package/dist/index.d.ts +24 -0
  56. package/dist/index.js +29 -0
  57. package/dist/internal.d.ts +9 -0
  58. package/dist/internal.js +9 -0
  59. package/dist/llm.d.ts +2 -0
  60. package/dist/llm.js +2 -0
  61. package/dist/local.d.ts +24 -0
  62. package/dist/local.js +58 -0
  63. package/dist/mcp.d.ts +2 -0
  64. package/dist/mcp.js +2 -0
  65. package/dist/protocol.d.ts +2 -0
  66. package/dist/protocol.js +2 -0
  67. package/dist/tools.d.ts +2 -0
  68. package/dist/tools.js +2 -0
  69. package/dist/tracing.d.ts +2 -0
  70. package/dist/tracing.js +2 -0
  71. package/package.json +78 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Process-level service registry with handle indirection — the substrate for hot-swapping
3
+ * shared instances (design: docs/architecture.md §5.5).
4
+ *
5
+ * Consumers never hold a service instance. `handle()` returns a stable Proxy that resolves
6
+ * the CURRENT table entry on every method call and holds a lease for exactly that call, so
7
+ * replacing a provider is a registry-local act — swap the entry, drain the old instance's
8
+ * leases, dispose it — with no session participation at all. The invariant this file exists
9
+ * to keep: at any moment only one generation of a service is being handed out.
10
+ *
11
+ * Handles expose METHODS ONLY. A method's return value must not carry the instance (no
12
+ * `return this`, no objects closing over it) — that is the one leak the Proxy cannot stop.
13
+ */
14
+ export type ServiceUnavailableReason = "missing" | "draining";
15
+ export declare class ServiceUnavailableError extends Error {
16
+ readonly serviceName: string;
17
+ readonly reason: ServiceUnavailableReason;
18
+ constructor(serviceName: string, reason: ServiceUnavailableReason);
19
+ }
20
+ export interface ServiceOptions {
21
+ /** Absent/false = `replace()` rejects; swapping this service means restarting the process. */
22
+ readonly replaceable?: boolean;
23
+ /** Called once the old instance has drained. Default: call `instance.close()` when present. */
24
+ readonly dispose?: (instance: unknown) => void | Promise<void>;
25
+ }
26
+ export declare class ServiceRegistry {
27
+ private readonly table;
28
+ private readonly handles;
29
+ /** Per-name op chain: concurrent replace/unregister serialize instead of interleaving. */
30
+ private readonly ops;
31
+ private readonly warn;
32
+ constructor(options?: {
33
+ readonly warn?: (message: string) => void;
34
+ });
35
+ /** Register a service under a unique name. Duplicate names fail closed — an extension that
36
+ * wants to take over an existing name must go through `replace`, never shadow. */
37
+ register(name: string, instance: unknown, options?: ServiceOptions): void;
38
+ has(name: string): boolean;
39
+ /**
40
+ * The stable handle for a service — safe to take before the service is registered and to
41
+ * keep across replaces. Property access requires a method (a non-function property throws:
42
+ * replaceable services expose methods only); availability is judged at CALL time, so a
43
+ * handle taken during early setup never explodes on mere property access.
44
+ */
45
+ handle<T = unknown>(name: string): T;
46
+ /**
47
+ * Swap the provider behind a name. Atomic from the consumers' side: the very next handle
48
+ * call lands on `next`; calls already in flight finish on the old instance, which is
49
+ * disposed once its leases drain (or after `drainTimeoutMs`, logged, default 10s).
50
+ *
51
+ * `force` skips the `replaceable` gate. It exists for ONE caller — `harness.replaceExtension`,
52
+ * where the extension that OWNS this service is being replaced along with it, inside the
53
+ * barrier's quiet moment. The gate guards the other case (a host swapping an instance under a
54
+ * live owner that never agreed to it), which is why it stays closed by default.
55
+ */
56
+ replace(name: string, next: unknown, options?: {
57
+ readonly drainTimeoutMs?: number;
58
+ readonly force?: boolean;
59
+ }): Promise<void>;
60
+ /**
61
+ * Remove a service entirely (extension unload). Unlike `replace`, callers see "draining"
62
+ * during the drain and "missing" after — there is deliberately no replaceable gate here:
63
+ * unload is the host/loader tearing an extension's service down, not swapping code under consumers.
64
+ */
65
+ unregister(name: string, options?: {
66
+ readonly drainTimeoutMs?: number;
67
+ }): Promise<void>;
68
+ private invoke;
69
+ private enqueueOp;
70
+ private retire;
71
+ private drainLeases;
72
+ private disposeOf;
73
+ }
74
+ /** A handle whose every call throws "missing" — handed to detached extensions and to
75
+ * extensions on hosts that expose no services, mirroring the actions revocation collar. */
76
+ export declare function deadServiceHandle<T = unknown>(name: string): T;
77
+ /**
78
+ * Properties a proxy must NEVER intercept with a throw or a fake method — the probe surface
79
+ * generic JS pokes at (await → `then`, JSON.stringify → `toJSON` via the absent-property rule,
80
+ * class checks → `prototype`/`constructor`, test/inspect utilities → `_`-prefixed, serializers →
81
+ * numeric indices, symbols everywhere). Cordis's reflect layer converged on this exact list
82
+ * over years of proxy bugs (`isSpecialProperty`); we adopt it wholesale.
83
+ */
84
+ export declare function isProbeProperty(prop: string | symbol): boolean;
85
+ //# sourceMappingURL=services.d.ts.map
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Process-level service registry with handle indirection — the substrate for hot-swapping
3
+ * shared instances (design: docs/architecture.md §5.5).
4
+ *
5
+ * Consumers never hold a service instance. `handle()` returns a stable Proxy that resolves
6
+ * the CURRENT table entry on every method call and holds a lease for exactly that call, so
7
+ * replacing a provider is a registry-local act — swap the entry, drain the old instance's
8
+ * leases, dispose it — with no session participation at all. The invariant this file exists
9
+ * to keep: at any moment only one generation of a service is being handed out.
10
+ *
11
+ * Handles expose METHODS ONLY. A method's return value must not carry the instance (no
12
+ * `return this`, no objects closing over it) — that is the one leak the Proxy cannot stop.
13
+ */
14
+ export class ServiceUnavailableError extends Error {
15
+ serviceName;
16
+ reason;
17
+ constructor(serviceName, reason) {
18
+ super(reason === "missing"
19
+ ? `service "${serviceName}" is not registered`
20
+ : `service "${serviceName}" is shutting down`);
21
+ this.name = "ServiceUnavailableError";
22
+ this.serviceName = serviceName;
23
+ this.reason = reason;
24
+ }
25
+ }
26
+ const DEFAULT_DRAIN_TIMEOUT_MS = 10_000;
27
+ export class ServiceRegistry {
28
+ table = new Map();
29
+ handles = new Map();
30
+ /** Per-name op chain: concurrent replace/unregister serialize instead of interleaving. */
31
+ ops = new Map();
32
+ warn;
33
+ constructor(options = {}) {
34
+ this.warn = options.warn ?? ((message) => console.warn(`[services] ${message}`));
35
+ }
36
+ /** Register a service under a unique name. Duplicate names fail closed — an extension that
37
+ * wants to take over an existing name must go through `replace`, never shadow. */
38
+ register(name, instance, options = {}) {
39
+ if (name.length === 0)
40
+ throw new Error("service name must be non-empty");
41
+ if (this.table.has(name))
42
+ throw new Error(`service "${name}" is already registered`);
43
+ this.table.set(name, {
44
+ instance,
45
+ generation: 1,
46
+ leases: 0,
47
+ draining: false,
48
+ replaceable: options.replaceable === true,
49
+ ...(options.dispose !== undefined ? { dispose: options.dispose } : {}),
50
+ });
51
+ }
52
+ has(name) {
53
+ const entry = this.table.get(name);
54
+ return entry !== undefined && !entry.draining;
55
+ }
56
+ /**
57
+ * The stable handle for a service — safe to take before the service is registered and to
58
+ * keep across replaces. Property access requires a method (a non-function property throws:
59
+ * replaceable services expose methods only); availability is judged at CALL time, so a
60
+ * handle taken during early setup never explodes on mere property access.
61
+ */
62
+ handle(name) {
63
+ const cached = this.handles.get(name);
64
+ if (cached !== undefined)
65
+ return cached;
66
+ const registry = this;
67
+ const proxy = new Proxy(Object.create(null), {
68
+ get(_target, prop) {
69
+ // Probe surface reads as undefined (await, JSON.stringify, inspect, test utilities —
70
+ // see isProbeProperty): probes must never throw or receive a fake method.
71
+ if (isProbeProperty(prop))
72
+ return undefined;
73
+ const key = prop; // symbols were filtered by the probe check
74
+ const entry = registry.table.get(name);
75
+ if (entry !== undefined) {
76
+ const value = entry.instance[key];
77
+ // Absent entirely → undefined (probe-safe, and a typo'd method fails naturally at
78
+ // the call site). Present but NOT a method → the discipline throw: field access is
79
+ // the one real violation ("replaceable services expose methods only").
80
+ if (value === undefined && !(key in entry.instance))
81
+ return undefined;
82
+ if (typeof value !== "function") {
83
+ throw new TypeError(`service "${name}": "${key}" is not a method — replaceable services expose methods only`);
84
+ }
85
+ }
86
+ return (...args) => registry.invoke(name, key, args);
87
+ },
88
+ has(_target, prop) {
89
+ const entry = registry.table.get(name);
90
+ return (entry !== undefined &&
91
+ typeof prop === "string" &&
92
+ typeof entry.instance[prop] === "function");
93
+ },
94
+ });
95
+ this.handles.set(name, proxy);
96
+ return proxy;
97
+ }
98
+ /**
99
+ * Swap the provider behind a name. Atomic from the consumers' side: the very next handle
100
+ * call lands on `next`; calls already in flight finish on the old instance, which is
101
+ * disposed once its leases drain (or after `drainTimeoutMs`, logged, default 10s).
102
+ *
103
+ * `force` skips the `replaceable` gate. It exists for ONE caller — `harness.replaceExtension`,
104
+ * where the extension that OWNS this service is being replaced along with it, inside the
105
+ * barrier's quiet moment. The gate guards the other case (a host swapping an instance under a
106
+ * live owner that never agreed to it), which is why it stays closed by default.
107
+ */
108
+ replace(name, next, options = {}) {
109
+ return this.enqueueOp(name, async () => {
110
+ const old = this.table.get(name);
111
+ if (old === undefined)
112
+ throw new Error(`cannot replace service "${name}": not registered`);
113
+ if (!old.replaceable && options.force !== true) {
114
+ throw new Error(`service "${name}" is not replaceable — register it with { replaceable: true }, or restart the process to change it`);
115
+ }
116
+ this.table.set(name, {
117
+ instance: next,
118
+ generation: old.generation + 1,
119
+ leases: 0,
120
+ draining: false,
121
+ replaceable: old.replaceable,
122
+ ...(old.dispose !== undefined ? { dispose: old.dispose } : {}),
123
+ });
124
+ await this.retire(old, name, options.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS);
125
+ });
126
+ }
127
+ /**
128
+ * Remove a service entirely (extension unload). Unlike `replace`, callers see "draining"
129
+ * during the drain and "missing" after — there is deliberately no replaceable gate here:
130
+ * unload is the host/loader tearing an extension's service down, not swapping code under consumers.
131
+ */
132
+ unregister(name, options = {}) {
133
+ return this.enqueueOp(name, async () => {
134
+ const entry = this.table.get(name);
135
+ if (entry === undefined)
136
+ return;
137
+ entry.draining = true;
138
+ await this.drainLeases(entry, name, options.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS);
139
+ this.table.delete(name);
140
+ await this.disposeOf(entry, name);
141
+ });
142
+ }
143
+ invoke(name, prop, args) {
144
+ const entry = this.table.get(name);
145
+ if (entry === undefined || entry.draining) {
146
+ throw new ServiceUnavailableError(name, entry === undefined ? "missing" : "draining");
147
+ }
148
+ const fn = entry.instance[prop];
149
+ if (typeof fn !== "function") {
150
+ throw new TypeError(`service "${name}": "${prop}" is not a method — replaceable services expose methods only`);
151
+ }
152
+ // The lease covers exactly this call. Resolve-at-call is what makes a swap between two
153
+ // calls land the next call on the new instance with zero consumer cooperation.
154
+ entry.leases += 1;
155
+ let released = false;
156
+ const release = () => {
157
+ if (released)
158
+ return;
159
+ released = true;
160
+ entry.leases -= 1;
161
+ if (entry.leases === 0 && entry.draining)
162
+ entry.onDrained?.();
163
+ };
164
+ let result;
165
+ try {
166
+ result = fn.apply(entry.instance, args);
167
+ }
168
+ catch (error) {
169
+ release();
170
+ throw error;
171
+ }
172
+ if (isPromiseLike(result))
173
+ return Promise.resolve(result).finally(release);
174
+ release();
175
+ return result;
176
+ }
177
+ enqueueOp(name, run) {
178
+ const prev = this.ops.get(name) ?? Promise.resolve();
179
+ const next = prev.then(() => run(), () => run());
180
+ // The chain must survive a rejected op; the rejection still reaches that op's caller.
181
+ this.ops.set(name, next.catch(() => undefined));
182
+ return next;
183
+ }
184
+ async retire(old, name, drainTimeoutMs) {
185
+ old.draining = true;
186
+ await this.drainLeases(old, name, drainTimeoutMs);
187
+ await this.disposeOf(old, name);
188
+ }
189
+ async drainLeases(entry, name, timeoutMs) {
190
+ if (entry.leases === 0)
191
+ return;
192
+ const drained = new Promise((resolve) => {
193
+ entry.onDrained = resolve;
194
+ });
195
+ // Re-check after wiring the callback: the last lease may have released in between.
196
+ if (entry.leases === 0)
197
+ return;
198
+ let timer;
199
+ const timedOut = await Promise.race([
200
+ drained.then(() => false),
201
+ new Promise((resolve) => {
202
+ timer = setTimeout(() => resolve(true), timeoutMs);
203
+ }),
204
+ ]);
205
+ clearTimeout(timer);
206
+ if (timedOut) {
207
+ this.warn(`service "${name}": disposing with ${entry.leases} lease(s) still outstanding after ${timeoutMs}ms drain timeout`);
208
+ }
209
+ }
210
+ async disposeOf(entry, name) {
211
+ try {
212
+ if (entry.dispose !== undefined) {
213
+ await entry.dispose(entry.instance);
214
+ }
215
+ else {
216
+ const close = entry.instance?.close;
217
+ if (typeof close === "function")
218
+ await close.call(entry.instance);
219
+ }
220
+ }
221
+ catch (error) {
222
+ this.warn(`service "${name}": dispose failed: ${error instanceof Error ? error.message : String(error)}`);
223
+ }
224
+ }
225
+ }
226
+ /** A handle whose every call throws "missing" — handed to detached extensions and to
227
+ * extensions on hosts that expose no services, mirroring the actions revocation collar. */
228
+ export function deadServiceHandle(name) {
229
+ return new Proxy(Object.create(null), {
230
+ get(_target, prop) {
231
+ if (isProbeProperty(prop))
232
+ return undefined;
233
+ return () => {
234
+ throw new ServiceUnavailableError(name, "missing");
235
+ };
236
+ },
237
+ // `in` is the registration probe (thin shells use it to fall back to a direct instance);
238
+ // a dead handle answers no to everything.
239
+ has() {
240
+ return false;
241
+ },
242
+ });
243
+ }
244
+ /**
245
+ * Properties a proxy must NEVER intercept with a throw or a fake method — the probe surface
246
+ * generic JS pokes at (await → `then`, JSON.stringify → `toJSON` via the absent-property rule,
247
+ * class checks → `prototype`/`constructor`, test/inspect utilities → `_`-prefixed, serializers →
248
+ * numeric indices, symbols everywhere). Cordis's reflect layer converged on this exact list
249
+ * over years of proxy bugs (`isSpecialProperty`); we adopt it wholesale.
250
+ */
251
+ export function isProbeProperty(prop) {
252
+ return (typeof prop === "symbol" ||
253
+ prop === "then" ||
254
+ prop === "toJSON" ||
255
+ prop === "prototype" ||
256
+ prop === "constructor" ||
257
+ String(parseInt(prop, 10)) === prop ||
258
+ prop.startsWith("_"));
259
+ }
260
+ function isPromiseLike(value) {
261
+ return ((typeof value === "object" || typeof value === "function") &&
262
+ value !== null &&
263
+ typeof value.then === "function");
264
+ }
265
+ //# sourceMappingURL=services.js.map