@lensagent/core 0.1.0 → 0.1.2

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 (99) hide show
  1. package/README.md +63 -4
  2. package/config/lens.example.jsonc +36 -13
  3. package/dist/adapters/executor-router.d.ts +20 -0
  4. package/dist/adapters/executor-router.js +86 -0
  5. package/dist/adapters/executor.d.ts +9 -3
  6. package/dist/adapters/executor.js +196 -61
  7. package/dist/adapters/lsp.d.ts +2 -1
  8. package/dist/adapters/lsp.js +98 -41
  9. package/dist/adapters/mcp.js +52 -17
  10. package/dist/adapters/revert.d.ts +5 -4
  11. package/dist/adapters/revert.js +6 -64
  12. package/dist/adapters/web.d.ts +9 -0
  13. package/dist/adapters/web.js +244 -0
  14. package/dist/config/files.d.ts +14 -0
  15. package/dist/config/files.js +57 -0
  16. package/dist/config/runtime.d.ts +3 -4
  17. package/dist/config/runtime.js +116 -97
  18. package/dist/config/schema.d.ts +56 -10
  19. package/dist/config/schema.js +48 -8
  20. package/dist/contracts/capability.d.ts +27 -1
  21. package/dist/contracts/domain.d.ts +1 -0
  22. package/dist/contracts/model.d.ts +33 -6
  23. package/dist/contracts/review.d.ts +17 -0
  24. package/dist/contracts/review.js +1 -0
  25. package/dist/core/access-policy.d.ts +40 -0
  26. package/dist/core/access-policy.js +68 -0
  27. package/dist/core/access.d.ts +30 -2
  28. package/dist/core/access.js +159 -26
  29. package/dist/core/context-boundaries.d.ts +25 -0
  30. package/dist/core/context-boundaries.js +106 -0
  31. package/dist/core/context.d.ts +3 -0
  32. package/dist/core/context.js +11 -0
  33. package/dist/core/host.js +261 -173
  34. package/dist/entry/pair.d.ts +1 -0
  35. package/dist/entry/pair.js +14 -0
  36. package/dist/entry/run.js +1 -1
  37. package/dist/execution/lsp-process.d.ts +3 -2
  38. package/dist/execution/lsp-process.js +46 -28
  39. package/dist/http/server.d.ts +5 -0
  40. package/dist/http/server.js +262 -80
  41. package/dist/runtime/agent.d.ts +9 -4
  42. package/dist/runtime/agent.js +361 -286
  43. package/dist/runtime/artifacts.d.ts +13 -1
  44. package/dist/runtime/artifacts.js +137 -10
  45. package/dist/runtime/cache.d.ts +59 -16
  46. package/dist/runtime/cache.js +124 -43
  47. package/dist/runtime/calls.d.ts +1 -1
  48. package/dist/runtime/calls.js +164 -48
  49. package/dist/runtime/capabilities.d.ts +2 -2
  50. package/dist/runtime/capabilities.js +4 -5
  51. package/dist/runtime/cleanup.d.ts +5 -2
  52. package/dist/runtime/cleanup.js +48 -17
  53. package/dist/runtime/compaction.d.ts +9 -0
  54. package/dist/runtime/compaction.js +52 -0
  55. package/dist/runtime/context.d.ts +8 -4
  56. package/dist/runtime/context.js +115 -128
  57. package/dist/runtime/events.d.ts +7 -0
  58. package/dist/runtime/events.js +83 -0
  59. package/dist/runtime/images.d.ts +14 -0
  60. package/dist/runtime/images.js +82 -0
  61. package/dist/runtime/kernel-capabilities.d.ts +7 -1
  62. package/dist/runtime/kernel-capabilities.js +146 -68
  63. package/dist/runtime/model-audit.d.ts +9 -0
  64. package/dist/runtime/model-audit.js +39 -0
  65. package/dist/runtime/model.d.ts +109 -4
  66. package/dist/runtime/model.js +368 -143
  67. package/dist/runtime/observability.d.ts +1 -0
  68. package/dist/runtime/observability.js +60 -15
  69. package/dist/runtime/policy.d.ts +1 -1
  70. package/dist/runtime/policy.js +2 -2
  71. package/dist/runtime/profile.d.ts +2 -1
  72. package/dist/runtime/profile.js +12 -15
  73. package/dist/runtime/projects.d.ts +3 -1
  74. package/dist/runtime/projects.js +38 -17
  75. package/dist/runtime/ptc-worker.d.ts +1 -0
  76. package/dist/runtime/ptc-worker.js +97 -0
  77. package/dist/runtime/ptc.d.ts +16 -0
  78. package/dist/runtime/ptc.js +128 -0
  79. package/dist/runtime/results.d.ts +5 -4
  80. package/dist/runtime/results.js +29 -51
  81. package/dist/runtime/review.d.ts +4 -1
  82. package/dist/runtime/review.js +101 -22
  83. package/dist/runtime/scheduler.js +1 -0
  84. package/dist/runtime/skills.js +34 -5
  85. package/dist/runtime/task-audit.d.ts +21 -0
  86. package/dist/runtime/task-audit.js +122 -0
  87. package/dist/runtime/time.d.ts +9 -0
  88. package/dist/runtime/time.js +18 -0
  89. package/dist/runtime/tokenizer.d.ts +2 -0
  90. package/dist/runtime/tokenizer.js +26 -1
  91. package/dist/store/database.js +26 -1
  92. package/dist/store/event-log.d.ts +1 -0
  93. package/dist/store/event-log.js +22 -5
  94. package/dist/store/schema.d.ts +1 -1
  95. package/dist/store/schema.js +50 -1
  96. package/dist/store/store.d.ts +65 -19
  97. package/dist/store/store.js +986 -188
  98. package/package.json +18 -8
  99. package/proto/executor.proto +69 -55
@@ -12,6 +12,11 @@ export class Observability {
12
12
  #sequence = 0;
13
13
  #writeTail = Promise.resolve();
14
14
  #retentionChecked = false;
15
+ #queuedRecords = 0;
16
+ #queuedBytes = 0;
17
+ #lastWriteError = null;
18
+ #lastWarningAt = 0;
19
+ #closed = false;
15
20
  constructor(root, config) {
16
21
  this.#root = root;
17
22
  this.#capacity = config?.traces?.capacity ?? 2048;
@@ -21,23 +26,45 @@ export class Observability {
21
26
  const record = {
22
27
  sequence: ++this.#sequence,
23
28
  occurred_at: now(),
24
- component,
25
- operation,
26
- outcome,
29
+ component: component.slice(0, 128),
30
+ operation: operation.slice(0, 256),
31
+ outcome: outcome.slice(0, 128),
27
32
  duration_ms: durationMs,
28
- fields
33
+ fields: safeFields(fields)
29
34
  };
30
35
  this.#push(this.#logs, record);
31
36
  this.#push(this.#traces, record);
32
37
  if (component === 'agent' || component === 'call')
33
38
  this.#push(this.#agentOps, record);
34
39
  this.increment(`${component}_${operation}_${outcome}`);
35
- if (this.#logConfig?.enabled === true) {
36
- this.#writeTail = this.#writeTail.then(() => this.#write(record)).catch(() => undefined);
40
+ if (this.#logConfig?.enabled === true && !this.#closed) {
41
+ const bytes = Buffer.byteLength(JSON.stringify(record));
42
+ if (this.#queuedRecords >= (this.#logConfig.maxQueuedRecords ?? 1024) || this.#queuedBytes + bytes > (this.#logConfig.maxQueuedBytes ?? 4194304)) {
43
+ this.increment('log_records_dropped');
44
+ return;
45
+ }
46
+ this.#queuedRecords++;
47
+ this.#queuedBytes += bytes;
48
+ this.#writeTail = this.#writeTail.then(() => this.#write(record)).catch((error) => {
49
+ this.#lastWriteError = typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code.slice(0, 64) : 'LOG_WRITE_FAILED';
50
+ this.increment('log_write_failures');
51
+ if (Date.now() - this.#lastWarningAt > 60000) {
52
+ this.#lastWarningAt = Date.now();
53
+ process.stderr.write('Lens observability: log sink failed; see runtime diagnostics.\n');
54
+ }
55
+ }).finally(() => { this.#queuedRecords--; this.#queuedBytes -= bytes; });
37
56
  }
38
57
  }
39
58
  increment(name, amount = 1) {
40
- this.#metrics.set(metricName(name), (this.#metrics.get(metricName(name)) ?? 0) + amount);
59
+ const key = metricName(name).slice(0, 128);
60
+ if (!this.#metrics.has(key) && this.#metrics.size >= 1024) {
61
+ this.#metrics.set('metric_series_dropped', (this.#metrics.get('metric_series_dropped') ?? 0) + 1);
62
+ return;
63
+ }
64
+ this.#metrics.set(key, (this.#metrics.get(key) ?? 0) + amount);
65
+ }
66
+ status() {
67
+ return { queued_records: this.#queuedRecords, queued_bytes: this.#queuedBytes, last_write_error: this.#lastWriteError, metric_series: this.#metrics.size };
41
68
  }
42
69
  logs(limit) {
43
70
  return this.#logs.slice(-limit);
@@ -54,6 +81,7 @@ export class Observability {
54
81
  .join('\n') + '\n';
55
82
  }
56
83
  async close() {
84
+ this.#closed = true;
57
85
  await this.#writeTail;
58
86
  }
59
87
  #push(target, record) {
@@ -71,17 +99,18 @@ export class Observability {
71
99
  this.#retentionChecked = true;
72
100
  await pruneLogs(path, config.retentionDays);
73
101
  }
74
- const size = await stat(path).then((entry) => entry.size, () => 0);
102
+ const size = await stat(path).then((entry) => entry.size, (error) => { if (missing(error))
103
+ return 0; throw error; });
75
104
  if (size >= config.maxFileBytes) {
76
105
  if (config.maxFiles === 1)
77
- await unlink(path).catch(() => undefined);
106
+ await allowMissing(unlink(path));
78
107
  else
79
- await unlink(`${path}.${config.maxFiles - 1}`).catch(() => undefined);
108
+ await allowMissing(unlink(`${path}.${config.maxFiles - 1}`));
80
109
  for (let index = config.maxFiles - 2; index >= 1; index -= 1) {
81
- await rename(`${path}.${index}`, `${path}.${index + 1}`).catch(() => undefined);
110
+ await allowMissing(rename(`${path}.${index}`, `${path}.${index + 1}`));
82
111
  }
83
112
  if (config.maxFiles > 1)
84
- await rename(path, `${path}.1`).catch(() => undefined);
113
+ await allowMissing(rename(path, `${path}.1`));
85
114
  }
86
115
  await appendFile(path, `${JSON.stringify(record)}\n`, 'utf8');
87
116
  }
@@ -90,15 +119,31 @@ async function pruneLogs(path, retentionDays) {
90
119
  const threshold = Date.now() - retentionDays * 86400000;
91
120
  const directory = dirname(path);
92
121
  const name = basename(path);
93
- for (const entry of await readdir(directory).catch(() => [])) {
122
+ for (const entry of await readdir(directory)) {
94
123
  if (entry !== name && !entry.startsWith(`${name}.`))
95
124
  continue;
96
125
  const candidate = resolve(directory, entry);
97
- const modified = await stat(candidate).then((value) => value.mtimeMs, () => Date.now());
126
+ const modified = await stat(candidate).then((value) => value.mtimeMs, (error) => { if (missing(error))
127
+ return Date.now(); throw error; });
98
128
  if (modified < threshold)
99
- await unlink(candidate).catch(() => undefined);
129
+ await allowMissing(unlink(candidate));
100
130
  }
101
131
  }
102
132
  function metricName(value) {
103
133
  return value.toLowerCase().replace(/[^a-z0-9_]/g, '_');
104
134
  }
135
+ // Operational records contain identifiers and bounded status facts, never request bodies.
136
+ function safeFields(fields) {
137
+ const allowed = new Set(['project_id', 'requested_project_id', 'agent_id', 'call_id', 'run_id', 'step_id', 'state', 'outcome', 'outcome_unknown', 'error_code', 'error_name', 'status', 'reason', 'grace_ms', 'interrupted', 'server_id']);
138
+ return Object.fromEntries(Object.entries(fields).filter(([key, value]) => allowed.has(key) && (value === null || typeof value !== 'object')).map(([key, value]) => [key, typeof value === 'string' ? value.slice(0, 256) : value]));
139
+ }
140
+ function missing(error) { return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT'; }
141
+ async function allowMissing(operation) {
142
+ try {
143
+ await operation;
144
+ }
145
+ catch (error) {
146
+ if (!missing(error))
147
+ throw error;
148
+ }
149
+ }
@@ -5,4 +5,4 @@ export interface PolicyDecision {
5
5
  readonly autoReview: boolean;
6
6
  }
7
7
  export declare function decidePolicy(policy: Policy, effect: EffectScope): PolicyDecision;
8
- export declare function policyPrompt(capabilityId: string): Json;
8
+ export declare function policyPrompt(capabilityId: string, reviewReason?: string): Json;
@@ -10,10 +10,10 @@ export function decidePolicy(policy, effect) {
10
10
  return { state: 'QUEUED', autoReview: false };
11
11
  fail('POLICY_DENIED', 403, `Policy ${policy} does not allow effect ${effect}.`);
12
12
  }
13
- export function policyPrompt(capabilityId) {
13
+ export function policyPrompt(capabilityId, reviewReason) {
14
14
  return {
15
15
  kind: 'policy_approval',
16
- question: `Approve the Capability call ${capabilityId}?`,
16
+ question: `Approve the Capability call ${capabilityId}?${reviewReason === undefined ? '' : `\nAuto review: ${reviewReason}`}`,
17
17
  options: [{ label: 'Approve', value: 'approve' }, { label: 'Reject', value: 'reject' }],
18
18
  allow_freeform: false,
19
19
  multi_select: false
@@ -5,4 +5,5 @@ export interface ResolvedProfile {
5
5
  readonly value: ModelHarnessProfile;
6
6
  }
7
7
  export declare function resolveProfile(config: HarnessConfig, id?: string): ResolvedProfile;
8
- export declare function resolveReasoningEffort(profile: ResolvedProfile, configured: string | null): string;
8
+ export declare function modelReasoningEfforts(config: HarnessConfig, reference: string): readonly string[];
9
+ export declare function resolveReasoningEffort(config: HarnessConfig, reference: string, configured: string | null): string;
@@ -20,13 +20,6 @@ const genericProfile = {
20
20
  toolDescriptionStyle: 'plain',
21
21
  recoveryConstraints: { partialResponseResume: 'forbidden', preserveReasoningState: false }
22
22
  },
23
- ptc: {
24
- enabledByDefault: false,
25
- maxToolCalls: 32,
26
- schemaVersion: 'programmatic-tool-run-v1',
27
- instruction: 'Run bounded tool-heavy work through the hosted programmatic tool runtime. Use only the capability IDs frozen in this request.'
28
- },
29
- effort: { allowed: ['none', 'minimal', 'low', 'medium', 'high'], default: 'medium' },
30
23
  cache: {
31
24
  enabledByDefault: false,
32
25
  minimumEligiblePrefixTokens: 1024,
@@ -38,15 +31,19 @@ export function resolveProfile(config, id = 'generic-v1') {
38
31
  const value = id === 'generic-v1' ? genericProfile : config.modelHarnessProfiles?.[id];
39
32
  if (value === undefined)
40
33
  fail('MODEL_PROFILE_NOT_FOUND', 503, `Model Harness Profile ${id} is not configured.`);
41
- if (!value.effort.allowed.includes(value.effort.default)) {
42
- fail('MODEL_PROFILE_INVALID', 503, `Model Harness Profile ${id} has an invalid default reasoning effort.`);
43
- }
44
34
  return { id, digest: digest(value), value };
45
35
  }
46
- export function resolveReasoningEffort(profile, configured) {
47
- const effort = configured ?? profile.value.effort.default;
48
- if (!profile.value.effort.allowed.includes(effort)) {
49
- fail('MODEL_EFFORT_UNSUPPORTED', 400, `Reasoning effort ${effort} is not supported by Model Harness Profile ${profile.id}.`);
36
+ export function modelReasoningEfforts(config, reference) {
37
+ const slash = reference.indexOf('/');
38
+ const provider = config.providers[reference.slice(0, slash)];
39
+ const model = provider?.models[reference.slice(slash + 1)];
40
+ if (provider === undefined || model === undefined)
41
+ fail('MODEL_NOT_FOUND', 404, `Model ${reference} is not configured.`);
42
+ return model.reasoningEfforts ?? provider.reasoningEfforts ?? [];
43
+ }
44
+ export function resolveReasoningEffort(config, reference, configured) {
45
+ if (configured === null || !modelReasoningEfforts(config, reference).includes(configured)) {
46
+ fail('MODEL_EFFORT_UNSUPPORTED', 400, `Reasoning effort ${configured ?? '(not selected)'} is not in the configured reasoningEfforts for ${reference}.`);
50
47
  }
51
- return effort;
48
+ return configured;
52
49
  }
@@ -18,6 +18,7 @@ export interface ProjectRuntime {
18
18
  readonly context: ContextEngine;
19
19
  readonly agents: AgentRuntime;
20
20
  contractDigest(): string;
21
+ capabilitiesDigest(): string;
21
22
  refreshSkills(): void;
22
23
  }
23
24
  export declare class ProjectRuntimes {
@@ -32,7 +33,8 @@ export declare class ProjectRuntimes {
32
33
  readonly target: AdapterTarget;
33
34
  readonly events: EventBus;
34
35
  readonly pinnedProjectId: string;
35
- constructor(store: Store, config: ConfigRuntime, scheduler: Scheduler, artifacts: ArtifactStore, models: AiModelGateway, reviewer: AutoReviewer, sharedCapabilities: readonly RegisteredCapability[], target: AdapterTarget, events: EventBus, pinnedProjectId: string);
36
+ readonly sharedWorkspace: (path: string, executorId?: string) => boolean;
37
+ constructor(store: Store, config: ConfigRuntime, scheduler: Scheduler, artifacts: ArtifactStore, models: AiModelGateway, reviewer: AutoReviewer, sharedCapabilities: readonly RegisteredCapability[], target: AdapterTarget, events: EventBus, pinnedProjectId: string, sharedWorkspace?: (path: string, executorId?: string) => boolean);
36
38
  get(projectId: string): ProjectRuntime;
37
39
  refresh(projectId: string): void;
38
40
  startIdleEviction(maxIdleMs?: number): void;
@@ -1,9 +1,11 @@
1
+ import { digest } from '../core/codec.js';
1
2
  import { CapabilityRegistry } from './capabilities.js';
2
3
  import { CallRuntime } from './calls.js';
3
4
  import { ContextEngine } from './context.js';
4
5
  import { AgentRuntime } from './agent.js';
5
6
  import { KernelAdapter, kernelCapabilities } from './kernel-capabilities.js';
6
7
  import { workspaceSkills } from './skills.js';
8
+ import { webCapabilities } from '../adapters/web.js';
7
9
  import { coreManifests, manifestSetDigest } from './manifests.js';
8
10
  export class ProjectRuntimes {
9
11
  store;
@@ -16,10 +18,11 @@ export class ProjectRuntimes {
16
18
  target;
17
19
  events;
18
20
  pinnedProjectId;
21
+ sharedWorkspace;
19
22
  #runtimes = new Map();
20
23
  #evictionTimer;
21
24
  #closed = false;
22
- constructor(store, config, scheduler, artifacts, models, reviewer, sharedCapabilities, target, events, pinnedProjectId) {
25
+ constructor(store, config, scheduler, artifacts, models, reviewer, sharedCapabilities, target, events, pinnedProjectId, sharedWorkspace = () => false) {
23
26
  this.store = store;
24
27
  this.config = config;
25
28
  this.scheduler = scheduler;
@@ -30,6 +33,7 @@ export class ProjectRuntimes {
30
33
  this.target = target;
31
34
  this.events = events;
32
35
  this.pinnedProjectId = pinnedProjectId;
36
+ this.sharedWorkspace = sharedWorkspace;
33
37
  }
34
38
  get(projectId) {
35
39
  if (this.#closed)
@@ -60,7 +64,10 @@ export class ProjectRuntimes {
60
64
  this.#evictionTimer = undefined;
61
65
  const runtimes = [...this.#runtimes.values()].map((entry) => entry.runtime);
62
66
  this.#runtimes.clear();
63
- await Promise.allSettled(runtimes.flatMap((runtime) => [runtime.agents.close(), runtime.calls.close()]));
67
+ const results = await Promise.allSettled(runtimes.flatMap((runtime) => [runtime.agents.close(), runtime.calls.close(runtime.projectId)]));
68
+ const failures = results.filter((result) => result.status === 'rejected');
69
+ if (failures.length > 0)
70
+ throw new AggregateError(failures.map((result) => result.reason), 'Project runtime cleanup failed.');
64
71
  }
65
72
  async #evictIdle(maxIdleMs) {
66
73
  const timestamp = Date.now();
@@ -71,34 +78,50 @@ export class ProjectRuntimes {
71
78
  entry.lastAccessedAt = timestamp;
72
79
  continue;
73
80
  }
74
- const lastAccessedAt = entry.lastAccessedAt;
75
- await Promise.allSettled([entry.runtime.agents.close(), entry.runtime.calls.close()]);
76
- if (entry.lastAccessedAt === lastAccessedAt && !this.store.hasActiveRuntimeObjects(projectId))
77
- this.#runtimes.delete(projectId);
81
+ this.#runtimes.delete(projectId);
82
+ await Promise.allSettled([entry.runtime.agents.close(), entry.runtime.calls.close(entry.runtime.projectId)]);
78
83
  }
79
84
  }
80
85
  #build(projectId) {
81
86
  const registry = new CapabilityRegistry(this.target);
82
87
  for (const capability of this.sharedCapabilities)
83
88
  registry.register(capability);
89
+ for (const capability of webCapabilities())
90
+ registry.register(capability);
84
91
  const context = new ContextEngine(this.store, registry);
85
92
  const calls = new CallRuntime(this.store, registry, this.scheduler, this.artifacts, this.events);
86
- const agents = new AgentRuntime(this.store, this.config, context, this.models, registry, calls, this.reviewer, this.events);
87
- const kernel = new KernelAdapter(this.store, registry, this.config, (targetProjectId, agentId) => this.get(targetProjectId).agents.wake(targetProjectId, agentId), (targetProjectId, input, signal) => this.get(targetProjectId).agents.createSubagent(targetProjectId, input, signal));
93
+ const agents = new AgentRuntime(this.store, this.config, context, this.models, registry, calls, this.reviewer, this.events, () => refreshSkills());
94
+ const kernel = new KernelAdapter(this.store, registry, this.config, (targetProjectId, agentId) => this.get(targetProjectId).agents.wake(targetProjectId, agentId), (targetProjectId, input, signal) => this.get(targetProjectId).agents.createSubagent(targetProjectId, input, signal), (targetProjectId, agentId, focus, signal) => this.get(targetProjectId).agents.compactFromTool(targetProjectId, agentId, focus, signal), (invocation) => agents.executeProgram(invocation), this.artifacts);
88
95
  for (const capability of kernelCapabilities(kernel))
89
96
  registry.register(capability);
97
+ const definitionsDigest = () => digest(JSON.parse(JSON.stringify(registry.list())));
90
98
  const refreshSkills = () => {
91
- registry.removeKind('skill');
99
+ const previous = definitionsDigest();
92
100
  const paths = this.store.listWorkspaces(projectId).flatMap((workspace) => {
93
101
  const value = workspace.paths;
94
- return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
102
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string' && this.sharedWorkspace(entry, typeof workspace.executor_id === 'string' ? workspace.executor_id : 'remote')) : [];
95
103
  });
96
- for (const capability of workspaceSkills(paths))
104
+ let skills;
105
+ try {
106
+ skills = workspaceSkills(paths);
107
+ }
108
+ catch {
109
+ // Keep the last complete catalog when a Workspace temporarily becomes inaccessible.
110
+ this.events.emit(projectId, this.store.stateRevision(), 'runtime.notice', { code: 'skill_refresh_failed', component: 'capabilities', message: 'Workspace Skills could not be refreshed; the previous catalog remains active.' });
111
+ return;
112
+ }
113
+ registry.removeKind('skill');
114
+ for (const capability of skills)
97
115
  registry.register(capability);
116
+ if (definitionsDigest() !== previous) {
117
+ this.events.emit(projectId, this.store.stateRevision(), 'runtime.notice', { code: 'snapshot_required', component: 'capabilities', project_capabilities_digest: definitionsDigest() });
118
+ }
98
119
  };
120
+ const contractDigest = () => digest({
121
+ manifests: manifestSetDigest([...coreManifests(this.config.current.config), ...registry.contractManifests()], this.target),
122
+ capabilities: JSON.parse(JSON.stringify(registry.list().filter(definition => definition.kind !== 'skill')))
123
+ });
99
124
  refreshSkills();
100
- for (const callId of this.store.queuedDirectCallIds(projectId))
101
- calls.dispatch(projectId, callId);
102
125
  return {
103
126
  projectId,
104
127
  registry,
@@ -106,10 +129,8 @@ export class ProjectRuntimes {
106
129
  context,
107
130
  agents,
108
131
  refreshSkills,
109
- contractDigest: () => manifestSetDigest([
110
- ...coreManifests(this.config.current.config),
111
- ...registry.contractManifests()
112
- ], this.target)
132
+ capabilitiesDigest: definitionsDigest,
133
+ contractDigest
113
134
  };
114
135
  }
115
136
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,97 @@
1
+ import { parentPort, workerData } from 'node:worker_threads';
2
+ import { stripTypeScriptTypes } from 'node:module';
3
+ import { getQuickJS } from 'quickjs-emscripten';
4
+ const port = parentPort;
5
+ if (port === null)
6
+ throw new Error('PTC must run in a worker.');
7
+ const input = workerData;
8
+ const module = await getQuickJS();
9
+ const runtime = module.newRuntime();
10
+ runtime.setMemoryLimit(64 * 1024 * 1024);
11
+ runtime.setMaxStackSize(1024 * 1024);
12
+ let deadline = Date.now() + input.timeoutMs;
13
+ runtime.setInterruptHandler(() => Date.now() >= deadline);
14
+ const vm = runtime.newContext();
15
+ const pending = new Map();
16
+ let sequence = 0;
17
+ let outputBytes = 0;
18
+ const logs = [];
19
+ let finished = false;
20
+ const isFinished = () => finished;
21
+ function report(error) {
22
+ if (isFinished())
23
+ return;
24
+ finished = true;
25
+ port?.postMessage({ type: 'error', error: error instanceof Error ? error.message : String(error) });
26
+ }
27
+ function pump() {
28
+ const result = runtime.executePendingJobs();
29
+ if (result.error !== undefined) {
30
+ const error = vm.dump(result.error);
31
+ result.error.dispose();
32
+ report(JSON.stringify(error));
33
+ }
34
+ }
35
+ try {
36
+ const call = vm.newFunction('__call', (name, args) => {
37
+ const deferred = vm.newPromise();
38
+ const id = sequence++;
39
+ pending.set(id, deferred);
40
+ port.postMessage({ type: 'call', id, name: vm.getString(name), arguments: vm.getString(args) });
41
+ return deferred.handle;
42
+ });
43
+ vm.setProp(vm.global, '__call', call);
44
+ call.dispose();
45
+ const log = vm.newFunction('__log', (value) => {
46
+ const text = vm.getString(value);
47
+ outputBytes += Buffer.byteLength(text);
48
+ if (outputBytes > 1024 * 1024)
49
+ throw new Error('PTC output exceeds 1 MiB; return selected data.');
50
+ logs.push(text);
51
+ });
52
+ vm.setProp(vm.global, '__log', log);
53
+ log.dispose();
54
+ const bootstrap = vm.evalCode(`
55
+ const tools = Object.freeze(Object.fromEntries(${JSON.stringify(input.capabilityIds)}.map(name => [name, async (args = {}) => JSON.parse(await __call(name, JSON.stringify(args)))])));
56
+ const console = Object.freeze({ log: (...args) => __log(args.map(x => typeof x === 'string' ? x : JSON.stringify(x)).join(' ')) });
57
+ `);
58
+ vm.unwrapResult(bootstrap).dispose();
59
+ port.on('message', (message) => {
60
+ if (isFinished())
61
+ return;
62
+ const deferred = pending.get(message.id);
63
+ if (deferred === undefined)
64
+ return;
65
+ pending.delete(message.id);
66
+ const value = message.error === undefined ? vm.newString(message.value ?? 'null') : vm.newError(message.error);
67
+ if (message.error === undefined)
68
+ deferred.resolve(value);
69
+ else
70
+ deferred.reject(value);
71
+ value.dispose();
72
+ deferred.dispose();
73
+ deadline = Date.now() + input.timeoutMs;
74
+ pump();
75
+ });
76
+ // Strip an async function body so top-level await/return and erasable TS annotations work.
77
+ const wrapped = `(async () => {\n${input.program}\n})()`;
78
+ const code = input.language === 'typescript' ? stripTypeScriptTypes(wrapped, { mode: 'transform' }) : wrapped;
79
+ const result = vm.unwrapResult(vm.evalCode(code, 'program.js'));
80
+ const settled = vm.resolvePromise(result);
81
+ pump();
82
+ const value = vm.unwrapResult(await settled);
83
+ if (!isFinished()) {
84
+ const content = vm.dump(value);
85
+ const serialized = JSON.stringify({ value: content ?? null, logs });
86
+ if (Buffer.byteLength(serialized) > 1024 * 1024)
87
+ throw new Error('PTC output exceeds 1 MiB; return selected data.');
88
+ finished = true;
89
+ port.postMessage({ type: 'done', value: serialized });
90
+ }
91
+ value.dispose();
92
+ result.dispose();
93
+ }
94
+ catch (error) {
95
+ report(error);
96
+ }
97
+ // Parent terminates the worker on every exit path, including pending promises and runaway jobs.
@@ -0,0 +1,16 @@
1
+ import { type Json } from '../core/domain.js';
2
+ export interface LocalProgram {
3
+ readonly program: string;
4
+ readonly language: 'javascript' | 'typescript';
5
+ readonly capabilityIds: readonly string[];
6
+ readonly timeoutMs: number;
7
+ readonly maxToolCalls: number;
8
+ readonly signal: AbortSignal;
9
+ readonly waiting?: {
10
+ read(): boolean;
11
+ subscribe(changed: () => void): () => void;
12
+ };
13
+ execute(capabilityId: string, args: Json, ordinal: number, signal: AbortSignal): Promise<Json>;
14
+ }
15
+ /** A fresh isolated interpreter per call. Only JSON crosses its tool bridge. */
16
+ export declare function runLocalProgram(input: LocalProgram): Promise<Json>;
@@ -0,0 +1,128 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { fail } from '../core/domain.js';
3
+ /** A fresh isolated interpreter per call. Only JSON crosses its tool bridge. */
4
+ export async function runLocalProgram(input) {
5
+ input.signal.throwIfAborted();
6
+ if (Buffer.byteLength(input.program) > 262144)
7
+ fail('PTC_INPUT_LIMIT', 400, 'Program exceeds 256 KiB.');
8
+ const controller = new AbortController();
9
+ const abort = () => controller.abort(input.signal.reason);
10
+ input.signal.addEventListener('abort', abort, { once: true });
11
+ const worker = new Worker(new URL(import.meta.url.endsWith('.ts') ? './ptc-worker.ts' : './ptc-worker.js', import.meta.url), {
12
+ workerData: { program: input.program, language: input.language, capabilityIds: input.capabilityIds, timeoutMs: input.timeoutMs },
13
+ execArgv: [], env: {}, resourceLimits: { maxOldGenerationSizeMb: 128, stackSizeMb: 4 }
14
+ });
15
+ const pending = new Set();
16
+ let count = 0;
17
+ let bridgeBytes = 0;
18
+ let stopped = false;
19
+ const isStopped = () => stopped;
20
+ let timer;
21
+ let unsubscribe;
22
+ try {
23
+ return await new Promise((resolve, reject) => {
24
+ const stop = (error) => {
25
+ if (isStopped())
26
+ return;
27
+ stopped = true;
28
+ controller.abort(error);
29
+ reject(error);
30
+ };
31
+ const failure = (code, message) => stop(Object.assign(new Error(message), { code }));
32
+ controller.signal.addEventListener('abort', () => stop(controller.signal.reason instanceof Error ? controller.signal.reason : new Error('Program cancelled.')), { once: true });
33
+ let remaining = input.timeoutMs;
34
+ let started = performance.now();
35
+ let paused = false;
36
+ const refreshDeadline = () => {
37
+ if (isStopped())
38
+ return;
39
+ const now = performance.now();
40
+ if (!paused)
41
+ remaining -= now - started;
42
+ started = now;
43
+ clearTimeout(timer);
44
+ try {
45
+ paused = input.waiting?.read() === true;
46
+ }
47
+ catch (error) {
48
+ stop(error instanceof Error ? error : new Error(String(error)));
49
+ return;
50
+ }
51
+ // Policy review and user input pause the remaining execution budget.
52
+ if (!paused)
53
+ timer = setTimeout(() => failure('PTC_TIMEOUT', 'Local program exceeded its deadline.'), Math.max(0, remaining));
54
+ };
55
+ unsubscribe = input.waiting?.subscribe(refreshDeadline);
56
+ refreshDeadline();
57
+ worker.on('error', (error) => stop(error));
58
+ worker.on('exit', (code) => { if (!isStopped())
59
+ failure('PTC_WORKER_EXIT', `Local program worker exited without a result (${code}).`); });
60
+ worker.on('message', (message) => {
61
+ if (isStopped())
62
+ return;
63
+ if (message.type === 'error') {
64
+ failure('PTC_EXECUTION_FAILED', message.error ?? 'Local program failed.');
65
+ return;
66
+ }
67
+ if (message.type === 'done') {
68
+ if (pending.size > 0) {
69
+ failure('PTC_UNAWAITED_TOOLS', 'Await all tool calls before returning.');
70
+ return;
71
+ }
72
+ stopped = true;
73
+ resolve(JSON.parse(message.value ?? 'null'));
74
+ return;
75
+ }
76
+ if (message.type !== 'call' || typeof message.name !== 'string' || typeof message.arguments !== 'string' || typeof message.id !== 'number') {
77
+ failure('PTC_BRIDGE_INVALID', 'Invalid tool bridge request.');
78
+ return;
79
+ }
80
+ if (!input.capabilityIds.includes(message.name) || message.name === 'programmatic_tool_run') {
81
+ failure('CAPABILITY_NOT_LOADED', 'Tool is not in this program capability set.');
82
+ return;
83
+ }
84
+ if (++count > input.maxToolCalls) {
85
+ failure('PTC_CALL_LIMIT', 'Local program exceeded its tool call limit.');
86
+ return;
87
+ }
88
+ const id = message.id;
89
+ const name = message.name;
90
+ const argumentsText = message.arguments;
91
+ bridgeBytes += Buffer.byteLength(argumentsText);
92
+ if (bridgeBytes > 32 * 1024 * 1024) {
93
+ failure('PTC_BRIDGE_LIMIT', 'Program tool traffic exceeds 32 MiB; narrow tool queries.');
94
+ return;
95
+ }
96
+ const execution = (async () => {
97
+ try {
98
+ const output = await input.execute(name, JSON.parse(argumentsText), count - 1, controller.signal);
99
+ if (isStopped())
100
+ return;
101
+ const value = JSON.stringify(output);
102
+ bridgeBytes += Buffer.byteLength(value);
103
+ if (bridgeBytes > 32 * 1024 * 1024) {
104
+ failure('PTC_BRIDGE_LIMIT', 'Program tool traffic exceeds 32 MiB; narrow tool queries.');
105
+ return;
106
+ }
107
+ worker.postMessage({ id, value });
108
+ }
109
+ catch (error) {
110
+ if (!isStopped())
111
+ worker.postMessage({ id, error: error instanceof Error ? error.message : String(error) });
112
+ }
113
+ })();
114
+ pending.add(execution);
115
+ void execution.finally(() => pending.delete(execution));
116
+ });
117
+ });
118
+ }
119
+ finally {
120
+ stopped = true;
121
+ clearTimeout(timer);
122
+ unsubscribe?.();
123
+ input.signal.removeEventListener('abort', abort);
124
+ controller.abort(new Error('Local program finished.'));
125
+ await worker.terminate();
126
+ await Promise.allSettled(pending);
127
+ }
128
+ }
@@ -1,7 +1,8 @@
1
- import { type ArtifactOutput, type CapabilityOutput, type Json } from '../core/domain.js';
2
- export interface NormalizedOutput {
1
+ import type { CapabilityOutput, Json } from '../core/domain.js';
2
+ import type { ArtifactStore } from './artifacts.js';
3
+ export interface StoredOutput {
3
4
  readonly content: Json;
4
- readonly artifacts: readonly ArtifactOutput[];
5
+ readonly artifacts: readonly string[];
5
6
  readonly truncated: boolean;
6
7
  }
7
- export declare function normalizeOutput(output: CapabilityOutput, limit: number): NormalizedOutput;
8
+ export declare function storeOutput(projectId: string, output: CapabilityOutput, store: ArtifactStore, preview?: boolean): Promise<StoredOutput>;