@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.
- package/README.md +63 -4
- package/config/lens.example.jsonc +36 -13
- package/dist/adapters/executor-router.d.ts +20 -0
- package/dist/adapters/executor-router.js +86 -0
- package/dist/adapters/executor.d.ts +9 -3
- package/dist/adapters/executor.js +196 -61
- package/dist/adapters/lsp.d.ts +2 -1
- package/dist/adapters/lsp.js +98 -41
- package/dist/adapters/mcp.js +52 -17
- package/dist/adapters/revert.d.ts +5 -4
- package/dist/adapters/revert.js +6 -64
- package/dist/adapters/web.d.ts +9 -0
- package/dist/adapters/web.js +244 -0
- package/dist/config/files.d.ts +14 -0
- package/dist/config/files.js +57 -0
- package/dist/config/runtime.d.ts +3 -4
- package/dist/config/runtime.js +116 -97
- package/dist/config/schema.d.ts +56 -10
- package/dist/config/schema.js +48 -8
- package/dist/contracts/capability.d.ts +27 -1
- package/dist/contracts/domain.d.ts +1 -0
- package/dist/contracts/model.d.ts +33 -6
- package/dist/contracts/review.d.ts +17 -0
- package/dist/contracts/review.js +1 -0
- package/dist/core/access-policy.d.ts +40 -0
- package/dist/core/access-policy.js +68 -0
- package/dist/core/access.d.ts +30 -2
- package/dist/core/access.js +159 -26
- package/dist/core/context-boundaries.d.ts +25 -0
- package/dist/core/context-boundaries.js +106 -0
- package/dist/core/context.d.ts +3 -0
- package/dist/core/context.js +11 -0
- package/dist/core/host.js +261 -173
- package/dist/entry/pair.d.ts +1 -0
- package/dist/entry/pair.js +14 -0
- package/dist/entry/run.js +1 -1
- package/dist/execution/lsp-process.d.ts +3 -2
- package/dist/execution/lsp-process.js +46 -28
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +262 -80
- package/dist/runtime/agent.d.ts +9 -4
- package/dist/runtime/agent.js +361 -286
- package/dist/runtime/artifacts.d.ts +13 -1
- package/dist/runtime/artifacts.js +137 -10
- package/dist/runtime/cache.d.ts +59 -16
- package/dist/runtime/cache.js +124 -43
- package/dist/runtime/calls.d.ts +1 -1
- package/dist/runtime/calls.js +164 -48
- package/dist/runtime/capabilities.d.ts +2 -2
- package/dist/runtime/capabilities.js +4 -5
- package/dist/runtime/cleanup.d.ts +5 -2
- package/dist/runtime/cleanup.js +48 -17
- package/dist/runtime/compaction.d.ts +9 -0
- package/dist/runtime/compaction.js +52 -0
- package/dist/runtime/context.d.ts +8 -4
- package/dist/runtime/context.js +115 -128
- package/dist/runtime/events.d.ts +7 -0
- package/dist/runtime/events.js +83 -0
- package/dist/runtime/images.d.ts +14 -0
- package/dist/runtime/images.js +82 -0
- package/dist/runtime/kernel-capabilities.d.ts +7 -1
- package/dist/runtime/kernel-capabilities.js +146 -68
- package/dist/runtime/model-audit.d.ts +9 -0
- package/dist/runtime/model-audit.js +39 -0
- package/dist/runtime/model.d.ts +109 -4
- package/dist/runtime/model.js +368 -143
- package/dist/runtime/observability.d.ts +1 -0
- package/dist/runtime/observability.js +60 -15
- package/dist/runtime/policy.d.ts +1 -1
- package/dist/runtime/policy.js +2 -2
- package/dist/runtime/profile.d.ts +2 -1
- package/dist/runtime/profile.js +12 -15
- package/dist/runtime/projects.d.ts +3 -1
- package/dist/runtime/projects.js +38 -17
- package/dist/runtime/ptc-worker.d.ts +1 -0
- package/dist/runtime/ptc-worker.js +97 -0
- package/dist/runtime/ptc.d.ts +16 -0
- package/dist/runtime/ptc.js +128 -0
- package/dist/runtime/results.d.ts +5 -4
- package/dist/runtime/results.js +29 -51
- package/dist/runtime/review.d.ts +4 -1
- package/dist/runtime/review.js +101 -22
- package/dist/runtime/scheduler.js +1 -0
- package/dist/runtime/skills.js +34 -5
- package/dist/runtime/task-audit.d.ts +21 -0
- package/dist/runtime/task-audit.js +122 -0
- package/dist/runtime/time.d.ts +9 -0
- package/dist/runtime/time.js +18 -0
- package/dist/runtime/tokenizer.d.ts +2 -0
- package/dist/runtime/tokenizer.js +26 -1
- package/dist/store/database.js +26 -1
- package/dist/store/event-log.d.ts +1 -0
- package/dist/store/event-log.js +22 -5
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.js +50 -1
- package/dist/store/store.d.ts +65 -19
- package/dist/store/store.js +986 -188
- package/package.json +18 -8
- package/proto/executor.proto +69 -55
package/dist/runtime/context.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { assertContextBoundaries, contextToolCalls } from '../core/context-boundaries.js';
|
|
2
|
+
import { canonicalize } from 'json-canonicalize';
|
|
1
3
|
import { digest, toolArguments } from '../core/codec.js';
|
|
4
|
+
import { activeContextItems } from '../core/context.js';
|
|
2
5
|
import { fail } from '../core/domain.js';
|
|
3
|
-
import { estimateRequest, estimateTokens } from './tokenizer.js';
|
|
6
|
+
import { estimateRequest, estimateTokens, tokenSlice } from './tokenizer.js';
|
|
7
|
+
import { resultImages } from './images.js';
|
|
4
8
|
export class ContextEngine {
|
|
5
9
|
store;
|
|
6
10
|
registry;
|
|
@@ -9,161 +13,144 @@ export class ContextEngine {
|
|
|
9
13
|
this.registry = registry;
|
|
10
14
|
}
|
|
11
15
|
capture(projectId, agentId) {
|
|
16
|
+
this.store.syncTodoReminder(projectId, agentId);
|
|
12
17
|
return this.store.contextSource(projectId, agentId);
|
|
13
18
|
}
|
|
14
19
|
prepare(projectId, agentId, config, contextWindowTokens, profile, requestFrame) {
|
|
15
|
-
return this.prepareSource(this.capture(projectId, agentId), config, contextWindowTokens, profile, requestFrame);
|
|
20
|
+
return this.prepareSource(this.capture(projectId, agentId), config, contextWindowTokens, profile, requestFrame, projectId);
|
|
16
21
|
}
|
|
17
|
-
prepareSource(source, config, contextWindowTokens, profile, requestFrame) {
|
|
18
|
-
const agentId = string(source.agent.id
|
|
19
|
-
const sourceRevision =
|
|
22
|
+
prepareSource(source, config, contextWindowTokens, profile, requestFrame, projectId) {
|
|
23
|
+
const agentId = string(source.agent.id);
|
|
24
|
+
const sourceRevision = Number(source.agent.context_revision);
|
|
25
|
+
const fullItems = activeContextItems(source.items);
|
|
26
|
+
let workItems = activeContextItems(source.workingItems ?? source.items);
|
|
27
|
+
const fullMessages = this.#messages(source, fullItems, profile);
|
|
28
|
+
let messages = this.#messages(source, workItems, profile);
|
|
29
|
+
assertContextBoundaries(fullMessages, { allowOpenTail: true, unsettledItemIds: source.unsettledItemIds ?? [] });
|
|
30
|
+
assertContextBoundaries(messages, { allowOpenTail: true, unsettledItemIds: source.unsettledItemIds ?? [] });
|
|
31
|
+
let estimate = estimateMessages(messages, requestFrame);
|
|
32
|
+
const decisions = [];
|
|
33
|
+
const edits = [];
|
|
34
|
+
if (estimate.expected >= contextWindowTokens * 0.6) {
|
|
35
|
+
const sourceTokens = estimate.expected;
|
|
36
|
+
const results = workItems.filter((item) => item.kind === 'result');
|
|
37
|
+
const olderIds = new Set(results.slice(0, Math.max(0, results.length - 15)).map((item) => item.id));
|
|
38
|
+
workItems = workItems.map((item) => {
|
|
39
|
+
if (!olderIds.has(item.id) || item.snipped === true || item.tool_name === 'programmatic_tool_run')
|
|
40
|
+
return item;
|
|
41
|
+
const content = snipResult(string(item.content));
|
|
42
|
+
if (content === item.content && resultImages(item, projectId ?? (typeof source.agent.project_id === 'string' ? source.agent.project_id : undefined), this.store).length === 0)
|
|
43
|
+
return item;
|
|
44
|
+
edits.push({ id: string(item.id), content });
|
|
45
|
+
// Everything except the payload is retained, including source identity and metadata.
|
|
46
|
+
return { ...item, content, snipped: true };
|
|
47
|
+
});
|
|
48
|
+
messages = this.#messages(source, workItems, profile);
|
|
49
|
+
estimate = estimateMessages(messages, requestFrame);
|
|
50
|
+
if (edits.length > 0)
|
|
51
|
+
decisions.push({ stage: 'snip', sourceTokens, projectedTokens: estimate.expected });
|
|
52
|
+
}
|
|
53
|
+
if (projectId !== undefined) {
|
|
54
|
+
this.store.saveWorkingContext(projectId, agentId, sourceRevision, edits, messages, messages.filter((message) => message.contextItemId === undefined), requestFrame ?? null);
|
|
55
|
+
}
|
|
56
|
+
return { source: { ...source, workingItems: workItems }, fullMessages, messages, estimate, sourceRevision, digest: digest(messages), decisions };
|
|
57
|
+
}
|
|
58
|
+
#messages(source, items, profile) {
|
|
20
59
|
const overrides = new Map(source.overrides.map((entry) => [`${string(entry.target_kind)}:${string(entry.target_id)}`, string(entry.instructions)]));
|
|
21
|
-
|
|
22
|
-
const latestCompaction = visibleItems.findLast((item) => item.kind === 'compaction');
|
|
23
|
-
const workItems = latestCompaction === undefined
|
|
24
|
-
? visibleItems
|
|
25
|
-
: [latestCompaction, ...visibleItems.filter((item) => item.id !== latestCompaction.id && item.locked !== true)];
|
|
26
|
-
let messages = workItems.map((item) => {
|
|
60
|
+
let messages = items.map((item) => {
|
|
27
61
|
const role = roleOf(item.role);
|
|
28
62
|
const content = string(item.content);
|
|
63
|
+
const metadata = { ...object(item.metadata) };
|
|
64
|
+
const response = metadata.model_response;
|
|
65
|
+
delete metadata.model_response;
|
|
66
|
+
const annotations = { contextItemId: string(item.id), metadata: { ...metadata, ...(item.snipped === true ? { snipped: true } : {}) } };
|
|
29
67
|
if (item.kind === 'tool_calls') {
|
|
30
|
-
const
|
|
31
|
-
if (!Array.isArray(calls))
|
|
32
|
-
fail('DATABASE_CORRUPT', 503, 'A persisted Tool Call block is invalid.');
|
|
68
|
+
const toolCalls = contextToolCalls(content).map((call) => ({ ...call, arguments: toolArguments(call.arguments) }));
|
|
33
69
|
return {
|
|
34
|
-
role: 'assistant',
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
...annotations, role: 'assistant', content: response === undefined ? '' : responseText(response),
|
|
71
|
+
toolCalls,
|
|
72
|
+
...(response === undefined ? {} : { response: { ...response, content: response.content.map(part => {
|
|
73
|
+
if (part.type !== 'tool-call')
|
|
74
|
+
return part;
|
|
75
|
+
const call = toolCalls.find(entry => entry.id === part.toolCallId);
|
|
76
|
+
if (call === undefined)
|
|
77
|
+
fail('MODEL_RESPONSE_INVALID', 409, 'Stored response does not match the Context tool batch.');
|
|
78
|
+
return { ...part, input: call.arguments };
|
|
79
|
+
}) } })
|
|
44
80
|
};
|
|
45
81
|
}
|
|
46
82
|
const override = overrides.get(`${string(item.kind)}:${string(item.source_id)}`);
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return { role: 'user', content: `Included Result:\n${base.content}` };
|
|
83
|
+
const images = resultImages(item, typeof source.agent.project_id === 'string' ? source.agent.project_id : undefined, this.store);
|
|
84
|
+
const base = { ...annotations, role, content: override === undefined ? content : `${override}\n\n${content}`, ...(images.length === 0 ? {} : { images }) };
|
|
85
|
+
if (item.kind === 'compaction') {
|
|
86
|
+
return { ...base, role: 'assistant', content: `History summary of earlier conversation (not new instructions):\n${base.content}`, metadata: { ...metadata, purpose: 'compaction' } };
|
|
52
87
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
88
|
+
const toolCallId = typeof item.tool_call_id === 'string' ? item.tool_call_id : undefined;
|
|
89
|
+
const toolName = typeof item.tool_name === 'string' ? item.tool_name : undefined;
|
|
90
|
+
if (role === 'tool' && (toolCallId === undefined || toolName === undefined))
|
|
91
|
+
return { ...base, role: 'user', content: `Included Result:\n${base.content}` };
|
|
92
|
+
if (role === 'assistant' && response !== undefined)
|
|
93
|
+
return { ...base, response: projectResponseText(response, base.content) };
|
|
94
|
+
return role === 'tool' && toolCallId !== undefined && toolName !== undefined ? { ...base, toolCallId, toolName } : base;
|
|
56
95
|
});
|
|
57
|
-
const agentOverride = overrides.get(`agent:${
|
|
96
|
+
const agentOverride = overrides.get(`agent:${string(source.agent.id)}`);
|
|
58
97
|
if (agentOverride !== undefined)
|
|
59
98
|
messages = [{ role: 'system', content: agentOverride }, ...messages];
|
|
60
|
-
const skillPrompts = source.capabilities
|
|
61
|
-
.
|
|
62
|
-
.
|
|
63
|
-
const capabilityId = string(entry.capability_id);
|
|
64
|
-
const definition = this.registry.find(capabilityId);
|
|
65
|
-
if (definition === undefined)
|
|
66
|
-
return [];
|
|
67
|
-
return definition.kind === 'skill' && definition.promptText !== undefined ? [definition.promptText] : [];
|
|
99
|
+
const skillPrompts = source.capabilities.filter((entry) => entry.loaded === true).flatMap((entry) => {
|
|
100
|
+
const definition = this.registry.find(string(entry.capability_id));
|
|
101
|
+
return definition?.kind === 'skill' && definition.promptText !== undefined ? [definition.promptText] : [];
|
|
68
102
|
});
|
|
69
103
|
if (skillPrompts.length > 0)
|
|
70
104
|
messages = [{ role: 'system', content: skillPrompts.join('\n\n') }, ...messages];
|
|
71
|
-
|
|
72
|
-
const reserve = config.contextBudget?.outputReserveTokens ?? 4096;
|
|
73
|
-
const margin = config.contextBudget?.safetyMarginTokens ?? 1024;
|
|
74
|
-
const maximum = contextWindowTokens - reserve - margin;
|
|
75
|
-
if (maximum < 1)
|
|
76
|
-
fail('CONTEXT_BUDGET_INVALID', 503, 'The configured context budget leaves no model input capacity.');
|
|
77
|
-
const decisions = [];
|
|
78
|
-
let estimate = estimateMessages(messages, requestFrame);
|
|
79
|
-
const projection = config.contextProjection;
|
|
80
|
-
if (estimate.safe > maximum && projection?.toolResultMaxTokens !== undefined) {
|
|
81
|
-
const sourceTokens = estimate.safe;
|
|
82
|
-
messages = toolResultBudget(messages, projection.toolResultMaxTokens);
|
|
83
|
-
estimate = estimateMessages(messages, requestFrame);
|
|
84
|
-
decisions.push({ stage: 'tool_result_budget', sourceTokens, projectedTokens: estimate.safe });
|
|
85
|
-
}
|
|
86
|
-
if (estimate.safe > maximum && projection?.snip !== undefined && estimate.safe > projection.snip.triggerTokens && messages.length > 4) {
|
|
87
|
-
const sourceTokens = estimate.safe;
|
|
88
|
-
const retainedBytes = Math.max(1, Math.floor(projection.snip.retainedTokens * 3 / Math.max(1, messages.length - 3)));
|
|
89
|
-
messages = messages.map((entry, index) => index > 0 && index < messages.length - 2 ? { ...entry, content: snip(entry.content, retainedBytes) } : entry);
|
|
90
|
-
estimate = estimateMessages(messages, requestFrame);
|
|
91
|
-
decisions.push({ stage: 'snip', sourceTokens, projectedTokens: estimate.safe });
|
|
92
|
-
}
|
|
93
|
-
if (estimate.safe > maximum && projection?.microcompact !== undefined && estimate.safe > projection.microcompact.triggerTokens && messages.length > 6) {
|
|
94
|
-
const sourceTokens = estimate.safe;
|
|
95
|
-
const head = first(messages);
|
|
96
|
-
const tail = messages.slice(-4);
|
|
97
|
-
messages = [head, { role: 'system', content: snip(localSummary(messages.slice(1, -4)), projection.microcompact.projectedTokens * 3) }, ...tail];
|
|
98
|
-
estimate = estimateMessages(messages, requestFrame);
|
|
99
|
-
decisions.push({ stage: 'microcompact', sourceTokens, projectedTokens: estimate.safe });
|
|
100
|
-
}
|
|
101
|
-
if (estimate.safe > maximum && projection?.contextCollapse !== undefined && estimate.safe > projection.contextCollapse.triggerTokens && messages.length > 2) {
|
|
102
|
-
const sourceTokens = estimate.safe;
|
|
103
|
-
const target = Math.min(maximum, projection.contextCollapse.projectedTokens);
|
|
104
|
-
while (messages.length > 2 && estimate.safe > target) {
|
|
105
|
-
messages = [first(messages), ...messages.slice(2)];
|
|
106
|
-
estimate = estimateMessages(messages, requestFrame);
|
|
107
|
-
}
|
|
108
|
-
decisions.push({ stage: 'context_collapse', sourceTokens, projectedTokens: estimate.safe });
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
source,
|
|
112
|
-
messages,
|
|
113
|
-
estimate,
|
|
114
|
-
sourceRevision,
|
|
115
|
-
digest: digest(messages),
|
|
116
|
-
decisions
|
|
117
|
-
};
|
|
105
|
+
return [{ role: 'system', content: profile.value.systemPrompt.content }, ...messages];
|
|
118
106
|
}
|
|
119
107
|
}
|
|
120
|
-
function
|
|
121
|
-
return
|
|
122
|
-
? estimateTokens(messages.map((entry) => `${entry.role}\n${entry.content}`).join('\n'))
|
|
123
|
-
: estimateRequest({ messages, ...requestFrame });
|
|
124
|
-
}
|
|
125
|
-
function toolResultBudget(messages, maximumTokens) {
|
|
126
|
-
const maximumBytes = maximumTokens * 3;
|
|
127
|
-
return messages.map((entry) => entry.role === 'tool' && Buffer.byteLength(entry.content) > maximumBytes
|
|
128
|
-
? { ...entry, content: `${Buffer.from(entry.content).subarray(0, maximumBytes).toString()}\n[tool result truncated]` }
|
|
129
|
-
: entry);
|
|
108
|
+
function responseText(response) {
|
|
109
|
+
return response.content.filter(part => part.type === 'text').map(part => part.text).join('');
|
|
130
110
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
111
|
+
/** Context text edits do not rewrite signatures or lose the original block ordering. */
|
|
112
|
+
function projectResponseText(response, text) {
|
|
113
|
+
if (responseText(response) === text)
|
|
114
|
+
return response;
|
|
115
|
+
const firstText = response.content.findIndex(part => part.type === 'text');
|
|
116
|
+
const content = response.content.flatMap((part, index) => {
|
|
117
|
+
if (part.type !== 'text')
|
|
118
|
+
return [part];
|
|
119
|
+
return index === firstText ? [{ ...part, text }] : [];
|
|
120
|
+
});
|
|
121
|
+
if (firstText === -1 && text.length > 0)
|
|
122
|
+
content.push({ type: 'text', text });
|
|
123
|
+
return { ...response, content };
|
|
137
124
|
}
|
|
138
|
-
function
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
function roleOf(value) {
|
|
142
|
-
if (value === 'system' || value === 'user' || value === 'assistant' || value === 'tool')
|
|
143
|
-
return value;
|
|
144
|
-
return 'user';
|
|
125
|
+
function estimateMessages(messages, requestFrame) {
|
|
126
|
+
return estimateRequest({ messages, ...requestFrame });
|
|
145
127
|
}
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
128
|
+
function snipResult(content) {
|
|
129
|
+
let envelope;
|
|
130
|
+
try {
|
|
131
|
+
envelope = object(JSON.parse(content));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return estimateTokens(content).expected > 1500 ? snippedText(content) : content;
|
|
135
|
+
}
|
|
136
|
+
if (envelope === null || envelope.content === undefined)
|
|
137
|
+
return content;
|
|
138
|
+
const payload = typeof envelope.content === 'string' ? envelope.content : canonicalize(envelope.content);
|
|
139
|
+
if (estimateTokens(payload).expected <= 1500)
|
|
140
|
+
return content;
|
|
141
|
+
return canonicalize({ ...envelope, content: snippedText(payload), truncated: true, payload: { ...object(envelope.payload), capture: object(envelope.payload)?.capture ?? 'unknown', stored: object(envelope.payload)?.stored ?? 'unknown', view: 'preview' } });
|
|
150
142
|
}
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
fail('DATABASE_CORRUPT', 503, `${field} is not a number.`);
|
|
154
|
-
return value;
|
|
143
|
+
function snippedText(payload) {
|
|
144
|
+
return `${tokenSlice(payload, 800, false)}\n\n[... snipped; original retained in Full Context ...]\n\n${tokenSlice(payload, 400, true)}`;
|
|
155
145
|
}
|
|
156
|
-
function
|
|
157
|
-
return typeof value === '
|
|
146
|
+
function object(value) {
|
|
147
|
+
return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
158
148
|
}
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
if (message === undefined)
|
|
162
|
-
fail('CONTEXT_EMPTY', 503, 'Prepared Context is empty.');
|
|
163
|
-
return message;
|
|
149
|
+
function roleOf(value) {
|
|
150
|
+
return value === 'system' || value === 'assistant' || value === 'tool' ? value : 'user';
|
|
164
151
|
}
|
|
165
|
-
function
|
|
152
|
+
function string(value) {
|
|
166
153
|
if (typeof value !== 'string')
|
|
167
|
-
fail('DATABASE_CORRUPT', 503, 'A
|
|
154
|
+
fail('DATABASE_CORRUPT', 503, 'A Context field is not a string.');
|
|
168
155
|
return value;
|
|
169
156
|
}
|
package/dist/runtime/events.d.ts
CHANGED
|
@@ -18,5 +18,12 @@ export declare class EventBus {
|
|
|
18
18
|
emit(projectId: string, stateRevision: number, type: RuntimeEvent['type'], payload: Json): void;
|
|
19
19
|
subscribe(clientId: string, projectId: string): Subscription;
|
|
20
20
|
revoke(clientId: string): void;
|
|
21
|
+
/** Internal committed-state notifications, independent of SSE buffers and client revocation. */
|
|
22
|
+
onProjectChange(projectId: string, changed: () => void, closed?: () => void): () => void;
|
|
23
|
+
/** Subscribe before reading committed state; undefined means the condition is still pending. */
|
|
24
|
+
waitFor<T>(projectId: string, read: () => T | undefined, signal: AbortSignal, timeout?: {
|
|
25
|
+
readonly ms: number;
|
|
26
|
+
readonly error: Error;
|
|
27
|
+
}): Promise<T>;
|
|
21
28
|
close(): void;
|
|
22
29
|
}
|
package/dist/runtime/events.js
CHANGED
|
@@ -5,6 +5,8 @@ export class EventBus {
|
|
|
5
5
|
metadataRevision;
|
|
6
6
|
#subscribers = new Map();
|
|
7
7
|
#lifecycle = new EventEmitter();
|
|
8
|
+
#waiters = new Map();
|
|
9
|
+
#closed = false;
|
|
8
10
|
#limits;
|
|
9
11
|
constructor(instanceEpoch, limits, metadataRevision) {
|
|
10
12
|
this.instanceEpoch = instanceEpoch;
|
|
@@ -15,6 +17,8 @@ export class EventBus {
|
|
|
15
17
|
this.#limits = limits;
|
|
16
18
|
}
|
|
17
19
|
publish(change) {
|
|
20
|
+
for (const waiter of this.#waiters.get(change.projectId) ?? [])
|
|
21
|
+
waiter.wake();
|
|
18
22
|
// Drops the two internal aliases and forwards the rest of `ChangedEntityRef` whole. Listing the
|
|
19
23
|
// fields to keep is what broke this twice: first it reprojected each entry down to `{ type, id }`
|
|
20
24
|
// and discarded the `read_route` a subscriber needs, then the replacement enumerated six fields
|
|
@@ -65,7 +69,86 @@ export class EventBus {
|
|
|
65
69
|
this.#close(subscriber);
|
|
66
70
|
}
|
|
67
71
|
}
|
|
72
|
+
/** Internal committed-state notifications, independent of SSE buffers and client revocation. */
|
|
73
|
+
onProjectChange(projectId, changed, closed = () => { }) {
|
|
74
|
+
if (this.#closed)
|
|
75
|
+
throw new Error('Event bus closed.');
|
|
76
|
+
const waiters = this.#waiters.get(projectId) ?? new Set();
|
|
77
|
+
this.#waiters.set(projectId, waiters);
|
|
78
|
+
let active = true;
|
|
79
|
+
let queued = false;
|
|
80
|
+
const unsubscribe = () => {
|
|
81
|
+
active = false;
|
|
82
|
+
waiters.delete(waiter);
|
|
83
|
+
if (waiters.size === 0)
|
|
84
|
+
this.#waiters.delete(projectId);
|
|
85
|
+
};
|
|
86
|
+
const waiter = {
|
|
87
|
+
wake: () => {
|
|
88
|
+
if (!active || queued)
|
|
89
|
+
return;
|
|
90
|
+
queued = true;
|
|
91
|
+
// Coalesce commits and never run database reads inside the publisher.
|
|
92
|
+
queueMicrotask(() => {
|
|
93
|
+
queued = false;
|
|
94
|
+
if (active)
|
|
95
|
+
changed();
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
close: () => { unsubscribe(); closed(); }
|
|
99
|
+
};
|
|
100
|
+
waiters.add(waiter);
|
|
101
|
+
return unsubscribe;
|
|
102
|
+
}
|
|
103
|
+
/** Subscribe before reading committed state; undefined means the condition is still pending. */
|
|
104
|
+
waitFor(projectId, read, signal, timeout) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
signal.throwIfAborted();
|
|
107
|
+
let settled = false;
|
|
108
|
+
let timer;
|
|
109
|
+
const cleanup = () => {
|
|
110
|
+
settled = true;
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
signal.removeEventListener('abort', abort);
|
|
113
|
+
unsubscribe();
|
|
114
|
+
};
|
|
115
|
+
const fail = (error) => {
|
|
116
|
+
if (settled)
|
|
117
|
+
return;
|
|
118
|
+
cleanup();
|
|
119
|
+
// AbortSignal.reason is caller-owned and need not be an Error.
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
|
121
|
+
reject(error);
|
|
122
|
+
};
|
|
123
|
+
const abort = () => fail(signal.reason);
|
|
124
|
+
const check = () => {
|
|
125
|
+
if (settled)
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
signal.throwIfAborted();
|
|
129
|
+
const value = read();
|
|
130
|
+
if (value !== undefined) {
|
|
131
|
+
cleanup();
|
|
132
|
+
resolve(value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
fail(error);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const unsubscribe = this.onProjectChange(projectId, check, () => fail(new Error('Event bus closed.')));
|
|
140
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
141
|
+
if (timeout !== undefined)
|
|
142
|
+
timer = setTimeout(() => fail(timeout.error), timeout.ms);
|
|
143
|
+
check();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
68
146
|
close() {
|
|
147
|
+
this.#closed = true;
|
|
148
|
+
for (const waiters of this.#waiters.values()) {
|
|
149
|
+
for (const waiter of waiters)
|
|
150
|
+
waiter.close();
|
|
151
|
+
}
|
|
69
152
|
for (const subscriber of this.#subscribers.values())
|
|
70
153
|
this.#close(subscriber);
|
|
71
154
|
this.#lifecycle.removeAllListeners();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ModelMessage as AiMessage } from 'ai';
|
|
2
|
+
import { type ModelImage, type ModelMessage } from '../core/domain.js';
|
|
3
|
+
import type { Document, Store } from '../store/store.js';
|
|
4
|
+
import type { ArtifactStore } from './artifacts.js';
|
|
5
|
+
export declare function isImageMediaType(value: unknown): value is string;
|
|
6
|
+
export declare const imageTokenEstimate = 4096;
|
|
7
|
+
export declare const imageByteLimit: number;
|
|
8
|
+
/** Only Result attachments are interpreted as images; ordinary message text is never parsed. */
|
|
9
|
+
export declare function resultImages(item: Document, projectId: string | undefined, store: Store): readonly ModelImage[];
|
|
10
|
+
export declare function imageReference(image: ModelImage): string;
|
|
11
|
+
/** Chat-compatible providers need images in user content, after the complete tool-result group. */
|
|
12
|
+
export declare function imageMessages(messages: readonly ModelMessage[]): ModelMessage[];
|
|
13
|
+
/** Keep binary data out of stored Context and attempt diagnostics; load verified bytes only for sending. */
|
|
14
|
+
export declare function materializeImages(messages: readonly AiMessage[], source: readonly ModelMessage[], artifacts?: ArtifactStore): Promise<AiMessage[]>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { fail } from '../core/domain.js';
|
|
2
|
+
const mediaTypes = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
3
|
+
export function isImageMediaType(value) {
|
|
4
|
+
return typeof value === 'string' && mediaTypes.has(value);
|
|
5
|
+
}
|
|
6
|
+
export const imageTokenEstimate = 4096;
|
|
7
|
+
export const imageByteLimit = 5 * 1024 * 1024;
|
|
8
|
+
/** Only Result attachments are interpreted as images; ordinary message text is never parsed. */
|
|
9
|
+
export function resultImages(item, projectId, store) {
|
|
10
|
+
if (item.kind !== 'result' || item.snipped === true)
|
|
11
|
+
return [];
|
|
12
|
+
let envelope;
|
|
13
|
+
try {
|
|
14
|
+
envelope = JSON.parse(item.content);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(envelope?.artifacts))
|
|
20
|
+
return [];
|
|
21
|
+
return envelope.artifacts.flatMap((id) => {
|
|
22
|
+
if (typeof id !== 'string')
|
|
23
|
+
return [];
|
|
24
|
+
if (projectId === undefined)
|
|
25
|
+
fail('DATABASE_CORRUPT', 503, 'Image Result is missing its Project.');
|
|
26
|
+
const artifact = store.getArtifact(projectId, id);
|
|
27
|
+
const mediaType = artifact.media_type;
|
|
28
|
+
if (!mediaTypes.has(mediaType))
|
|
29
|
+
return [];
|
|
30
|
+
if (Number(artifact.size_bytes) > imageByteLimit)
|
|
31
|
+
fail('IMAGE_TOO_LARGE', 413, 'Images must not exceed 5 MiB.');
|
|
32
|
+
return [{ projectId, artifactId: id }];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function imageReference(image) {
|
|
36
|
+
return `lens-artifact:${image.artifactId}`;
|
|
37
|
+
}
|
|
38
|
+
/** Chat-compatible providers need images in user content, after the complete tool-result group. */
|
|
39
|
+
export function imageMessages(messages) {
|
|
40
|
+
const result = [];
|
|
41
|
+
let pending = [];
|
|
42
|
+
let labels = [];
|
|
43
|
+
const flush = () => {
|
|
44
|
+
if (pending.length === 0)
|
|
45
|
+
return;
|
|
46
|
+
result.push({ role: 'user', content: `Images attached to tool results: ${labels.join(', ')}`, images: pending });
|
|
47
|
+
pending = [];
|
|
48
|
+
labels = [];
|
|
49
|
+
};
|
|
50
|
+
for (const message of messages) {
|
|
51
|
+
if (message.role !== 'tool')
|
|
52
|
+
flush();
|
|
53
|
+
result.push(message);
|
|
54
|
+
if (message.role === 'tool' && message.images?.length) {
|
|
55
|
+
pending.push(...message.images);
|
|
56
|
+
labels.push(`${message.toolName} (${message.toolCallId})`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
flush();
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/** Keep binary data out of stored Context and attempt diagnostics; load verified bytes only for sending. */
|
|
63
|
+
export async function materializeImages(messages, source, artifacts) {
|
|
64
|
+
const references = new Map(source.flatMap(message => (message.images ?? []).map(image => [imageReference(image), image])));
|
|
65
|
+
if (references.size === 0)
|
|
66
|
+
return [...messages];
|
|
67
|
+
if (artifacts === undefined)
|
|
68
|
+
fail('ARTIFACT_STORE_UNAVAILABLE', 503, 'Image input requires an Artifact store.');
|
|
69
|
+
const data = new Map();
|
|
70
|
+
for (const [reference, image] of references) {
|
|
71
|
+
const artifact = artifacts.store.getArtifact(image.projectId, image.artifactId);
|
|
72
|
+
const mediaType = artifact.media_type;
|
|
73
|
+
if (!isImageMediaType(mediaType))
|
|
74
|
+
fail('IMAGE_FORMAT_UNSUPPORTED', 422, 'Unsupported image format.');
|
|
75
|
+
if (Number(artifact.size_bytes) > imageByteLimit)
|
|
76
|
+
fail('IMAGE_TOO_LARGE', 413, 'Images must not exceed 5 MiB.');
|
|
77
|
+
data.set(reference, `data:${mediaType};base64,${(await artifacts.read(artifact)).toString('base64')}`);
|
|
78
|
+
}
|
|
79
|
+
return messages.map(message => message.role !== 'user' || typeof message.content === 'string' ? message : {
|
|
80
|
+
...message, content: message.content.map(part => part.type !== 'image' || typeof part.image !== 'string' ? part : { ...part, image: data.get(part.image) ?? part.image })
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ArtifactStore } from './artifacts.js';
|
|
1
2
|
import { type CapabilityAdapter, type CapabilityInvocation, type CapabilityOutput } from '../core/domain.js';
|
|
2
3
|
import type { ConfigRuntime } from '../config/runtime.js';
|
|
3
4
|
import type { Document, Store } from '../store/store.js';
|
|
@@ -16,7 +17,11 @@ export declare class KernelAdapter implements CapabilityAdapter {
|
|
|
16
17
|
readonly modelReasoningEffort?: string;
|
|
17
18
|
readonly initialPrompt?: string;
|
|
18
19
|
readonly initialMessage?: string;
|
|
20
|
+
readonly compactFocus?: string;
|
|
19
21
|
}, signal: AbortSignal) => Promise<Document>;
|
|
22
|
+
readonly compactAgent: (projectId: string, agentId: string, focus: string | undefined, signal: AbortSignal) => Promise<Document>;
|
|
23
|
+
readonly runProgram?: ((invocation: CapabilityInvocation) => Promise<CapabilityOutput>) | undefined;
|
|
24
|
+
readonly artifacts?: ArtifactStore | undefined;
|
|
20
25
|
readonly id = "kernel";
|
|
21
26
|
constructor(store: Store, registry: CapabilityRegistry, config: ConfigRuntime, wakeAgent: (projectId: string, agentId: string) => void, createSubagent: (projectId: string, input: {
|
|
22
27
|
readonly parentAgentId: string;
|
|
@@ -26,7 +31,8 @@ export declare class KernelAdapter implements CapabilityAdapter {
|
|
|
26
31
|
readonly modelReasoningEffort?: string;
|
|
27
32
|
readonly initialPrompt?: string;
|
|
28
33
|
readonly initialMessage?: string;
|
|
29
|
-
|
|
34
|
+
readonly compactFocus?: string;
|
|
35
|
+
}, signal: AbortSignal) => Promise<Document>, compactAgent: (projectId: string, agentId: string, focus: string | undefined, signal: AbortSignal) => Promise<Document>, runProgram?: ((invocation: CapabilityInvocation) => Promise<CapabilityOutput>) | undefined, artifacts?: ArtifactStore | undefined);
|
|
30
36
|
execute(invocation: CapabilityInvocation): Promise<CapabilityOutput>;
|
|
31
37
|
}
|
|
32
38
|
export declare function kernelCapabilities(adapter: KernelAdapter): readonly RegisteredCapability[];
|