newmark-agent 0.3.8 → 0.3.11
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/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.js +22 -0
- package/dist/context/domain/types.d.ts +1 -1
- package/dist/conversation-utility-host.bundle.cjs +2060 -774
- package/dist/conversation-utility-host.js +4 -1
- package/dist/core/agent.d.ts +23 -1
- package/dist/core/agent.js +517 -37
- package/dist/core/agentKernelRunner.js +20 -3
- package/dist/core/browserControl.d.ts +8 -0
- package/dist/core/browserUsePageAdapter.d.ts +3 -0
- package/dist/core/browserUsePageAdapter.js +19 -2
- package/dist/core/compressionHistoryArchive.d.ts +28 -0
- package/dist/core/compressionHistoryArchive.js +131 -0
- package/dist/core/computerUseSession.d.ts +44 -0
- package/dist/core/computerUseSession.js +105 -0
- package/dist/core/config.js +1 -0
- package/dist/core/conversationKernel.d.ts +3 -1
- package/dist/core/conversationKernel.js +76 -7
- package/dist/core/electronBrowserUseHost.js +16 -0
- package/dist/core/electronUtilityAgentClient.js +84 -2
- package/dist/core/electronUtilityRuntimePool.js +6 -7
- package/dist/core/runtimeLifecycle.d.ts +23 -0
- package/dist/core/runtimeLifecycle.js +146 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/core/utilityHostToolRouter.d.ts +7 -0
- package/dist/core/utilityHostToolRouter.js +25 -33
- package/dist/core/wslAgentRuntimePool.js +9 -10
- package/dist/launcher.js +13 -1
- package/dist/main.js +147 -18
- package/dist/preload.js +4 -2
- package/dist/tools/index.js +42 -52
- package/dist/tools/nativeTools.js +1 -1
- package/dist/ui/index.html +296 -40
- package/dist/wsl-agent-host.bundle.cjs +2063 -776
- package/dist/wsl-agent-host.js +4 -0
- package/package.json +3 -3
|
@@ -205,6 +205,9 @@ class ProviderRunError extends Error {
|
|
|
205
205
|
function kernelTurnFailed(agent, turn) {
|
|
206
206
|
return turn.stopReason === 'error' || agent.isLlmErrorText(turn.text);
|
|
207
207
|
}
|
|
208
|
+
function providerTurnIsEmpty(turn) {
|
|
209
|
+
return /provider returned an empty response/i.test(`${turn.errorMessage}\n${turn.text}`);
|
|
210
|
+
}
|
|
208
211
|
function removeTrailingFailedAssistant(agent, messages) {
|
|
209
212
|
const last = messages[messages.length - 1];
|
|
210
213
|
if (last?.role !== 'assistant')
|
|
@@ -338,10 +341,14 @@ async function runAgentKernel(agent) {
|
|
|
338
341
|
}
|
|
339
342
|
await kernel.prompt(promptMessages);
|
|
340
343
|
const assistant = lastAssistant;
|
|
344
|
+
const text = assistant ? KernelMessageText(assistant) : '';
|
|
345
|
+
const hasToolCall = !!assistant?.content?.some(content => content.type === 'toolCall');
|
|
346
|
+
const emptyResponse = !assistant
|
|
347
|
+
|| (!text.trim() && !hasToolCall && String(assistant?.stopReason || '') !== 'aborted');
|
|
341
348
|
return {
|
|
342
|
-
text:
|
|
349
|
+
text: emptyResponse ? '[Error] Provider returned an empty response.' : text,
|
|
343
350
|
stopReason: String(assistant?.stopReason || ''),
|
|
344
|
-
errorMessage: String(assistant?.errorMessage || ''),
|
|
351
|
+
errorMessage: String(assistant?.errorMessage || (emptyResponse ? 'Provider returned an empty response.' : '')),
|
|
345
352
|
};
|
|
346
353
|
}
|
|
347
354
|
finally {
|
|
@@ -380,6 +387,16 @@ async function runAgentKernel(agent) {
|
|
|
380
387
|
if (modelBeforeKernelRun && modelBeforeKernelRun !== agent.model && !tokens.some(t => t.text?.includes('[Model fallback]'))) {
|
|
381
388
|
tokens.unshift({ type: 'text', text: `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.` });
|
|
382
389
|
}
|
|
390
|
+
let emptyResponseRetries = 0;
|
|
391
|
+
while (providerTurnIsEmpty(lastTurn) && emptyResponseRetries < 2) {
|
|
392
|
+
removeTrailingFailedAssistant(agent, kernel.state.messages);
|
|
393
|
+
emptyResponseRetries += 1;
|
|
394
|
+
const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${emptyResponseRetries}/2).`;
|
|
395
|
+
tokens.push({ type: 'text', text: notice });
|
|
396
|
+
agent.recordWorkStatus(notice);
|
|
397
|
+
await agent.waitForPlannedRouteRetry();
|
|
398
|
+
lastTurn = await runWithCompressionResume([], false);
|
|
399
|
+
}
|
|
383
400
|
let routeRetries = 0;
|
|
384
401
|
while (kernelTurnFailed(agent, lastTurn) && routeRetries < 2) {
|
|
385
402
|
const previous = agent.switchToFallbackModel(lastTurn.errorMessage || lastTurn.text);
|
|
@@ -401,7 +418,7 @@ async function runAgentKernel(agent) {
|
|
|
401
418
|
throw new ProviderRunError(normalizePublicProviderError(lastTurn.errorMessage || lastTurn.text, [agent.activeModelConfig()?.api_key]));
|
|
402
419
|
}
|
|
403
420
|
const lastAssistant = lastTurn.text;
|
|
404
|
-
if (agent.mode === 'goal' && agent.goal && agent.goal.checkComplete(lastAssistant)) {
|
|
421
|
+
if (agent.mode === 'goal' && agent.goal && !agent.goal.paused && agent.goal.checkComplete(lastAssistant)) {
|
|
405
422
|
agent.markGoalComplete();
|
|
406
423
|
if (!tokens.some(token => token.type === 'text' && /goal complete/i.test(token.text || ''))) {
|
|
407
424
|
tokens.push({ type: 'text', text: '\n[Goal Complete]' });
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export type BrowserControlAction = 'open' | 'snapshot' | 'click' | 'type' | 'eval' | 'back' | 'forward' | 'reload' | 'cdp' | 'use';
|
|
2
|
+
/** Renderer/host routing identity. Browser controls must never fall back to the
|
|
3
|
+
* currently focused conversation when a caller has a concrete target. */
|
|
4
|
+
export interface BrowserControlTarget {
|
|
5
|
+
workspaceId?: string;
|
|
6
|
+
conversationId?: string;
|
|
7
|
+
runtimeKey?: string;
|
|
8
|
+
}
|
|
2
9
|
export interface BrowserControlRequest {
|
|
3
10
|
action: BrowserControlAction;
|
|
11
|
+
target?: BrowserControlTarget;
|
|
4
12
|
url?: string;
|
|
5
13
|
selector?: string;
|
|
6
14
|
text?: string;
|
|
@@ -22,6 +22,8 @@ export interface BrowserUseHostPage {
|
|
|
22
22
|
}>;
|
|
23
23
|
evaluateFixed<T>(script: string, signal?: AbortSignal): Promise<T>;
|
|
24
24
|
clickAt(x: number, y: number, signal?: AbortSignal): Promise<void>;
|
|
25
|
+
/** Optional deterministic DOM click for embedded guests where native input can be dropped while the host tab is settling. */
|
|
26
|
+
clickElement?(token: string, signal?: AbortSignal): Promise<void>;
|
|
25
27
|
replaceFocusedText(text: string, signal?: AbortSignal): Promise<void>;
|
|
26
28
|
pressKey(key: string, signal?: AbortSignal): Promise<void>;
|
|
27
29
|
navigate(url: string, signal?: AbortSignal): Promise<void>;
|
|
@@ -46,6 +48,7 @@ export type BrowserUseHostPageResolver = (scope: BrowserUseScope) => Promise<Bro
|
|
|
46
48
|
export declare function browserUseObservationScript(maxChars: number, maxRefs: number): string;
|
|
47
49
|
export declare function browserUseProbeScript(token: string, scrollIntoView?: boolean): string;
|
|
48
50
|
export declare function browserUseFocusScript(token: string): string;
|
|
51
|
+
export declare function browserUseClickScript(token: string): string;
|
|
49
52
|
export declare function browserUseSelectScript(token: string, value: string): string;
|
|
50
53
|
export declare function browserUseScrollScript(token: string | undefined, deltaX: number, deltaY: number): string;
|
|
51
54
|
export declare function browserUseExtractScript(token: string | undefined, attribute: string | undefined, maxChars: number): string;
|
|
@@ -4,6 +4,7 @@ exports.NativeBrowserUsePageAdapter = void 0;
|
|
|
4
4
|
exports.browserUseObservationScript = browserUseObservationScript;
|
|
5
5
|
exports.browserUseProbeScript = browserUseProbeScript;
|
|
6
6
|
exports.browserUseFocusScript = browserUseFocusScript;
|
|
7
|
+
exports.browserUseClickScript = browserUseClickScript;
|
|
7
8
|
exports.browserUseSelectScript = browserUseSelectScript;
|
|
8
9
|
exports.browserUseScrollScript = browserUseScrollScript;
|
|
9
10
|
exports.browserUseExtractScript = browserUseExtractScript;
|
|
@@ -186,6 +187,17 @@ function browserUseProbeScript(token, scrollIntoView = false) {
|
|
|
186
187
|
function browserUseFocusScript(token) {
|
|
187
188
|
return `(() => { const el = document.querySelector(${json(token)}); if (!el) return false; if (typeof el.scrollIntoView === 'function') el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'auto' }); el.focus({ preventScroll: true }); return document.activeElement === el || el.contains(document.activeElement); })()`;
|
|
188
189
|
}
|
|
190
|
+
function browserUseClickScript(token) {
|
|
191
|
+
return `(() => {
|
|
192
|
+
const el = document.querySelector(${json(token)});
|
|
193
|
+
if (!el) return { clicked: false, error: 'ref_not_found' };
|
|
194
|
+
if (typeof el.scrollIntoView === 'function') el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'auto' });
|
|
195
|
+
if (typeof el.focus === 'function') el.focus({ preventScroll: true });
|
|
196
|
+
if (typeof el.click !== 'function') return { clicked: false, error: 'not_clickable' };
|
|
197
|
+
el.click();
|
|
198
|
+
return { clicked: true, tag: String(el.localName || 'element').toLowerCase() };
|
|
199
|
+
})()`;
|
|
200
|
+
}
|
|
189
201
|
function browserUseSelectScript(token, value) {
|
|
190
202
|
return `(() => {
|
|
191
203
|
const el = document.querySelector(${json(token)});
|
|
@@ -299,8 +311,13 @@ class NativeBrowserUsePageAdapter {
|
|
|
299
311
|
throw error;
|
|
300
312
|
}
|
|
301
313
|
if (request.action === 'click') {
|
|
302
|
-
|
|
303
|
-
|
|
314
|
+
if (page.clickElement) {
|
|
315
|
+
await page.clickElement(request.element.token, signal);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const rect = probe.rect;
|
|
319
|
+
await page.clickAt(rect.x + rect.width / 2, rect.y + rect.height / 2, signal);
|
|
320
|
+
}
|
|
304
321
|
await page.waitForReady(signal);
|
|
305
322
|
return { clicked: true };
|
|
306
323
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface ArchivedCompressionEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
at: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
messages: Array<Record<string, unknown>>;
|
|
6
|
+
foldedEntries: number;
|
|
7
|
+
foldedChars: number;
|
|
8
|
+
model: string;
|
|
9
|
+
fallback: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Append-only cold storage for folded context segments evicted from the small
|
|
13
|
+
* in-state hot cache. The archive is never injected into a model request;
|
|
14
|
+
* callers must explicitly search/read/restore one bounded segment.
|
|
15
|
+
*/
|
|
16
|
+
export declare class CompressionHistoryArchive {
|
|
17
|
+
private readonly rootPath;
|
|
18
|
+
constructor(rootPath: string);
|
|
19
|
+
private file;
|
|
20
|
+
private append;
|
|
21
|
+
archive(scopeKey: string, entry: ArchivedCompressionEntry): void;
|
|
22
|
+
markRestored(scopeKey: string, id: string): void;
|
|
23
|
+
/** Replays the append-only ledger and returns only currently restorable entries. */
|
|
24
|
+
activeEntries(scopeKey: string): ArchivedCompressionEntry[];
|
|
25
|
+
/** Includes restored/tombstoned ids so a restart never reuses an archive id. */
|
|
26
|
+
maxNumericId(scopeKey: string): number;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=compressionHistoryArchive.d.ts.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CompressionHistoryArchive = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
/**
|
|
41
|
+
* Append-only cold storage for folded context segments evicted from the small
|
|
42
|
+
* in-state hot cache. The archive is never injected into a model request;
|
|
43
|
+
* callers must explicitly search/read/restore one bounded segment.
|
|
44
|
+
*/
|
|
45
|
+
class CompressionHistoryArchive {
|
|
46
|
+
rootPath;
|
|
47
|
+
constructor(rootPath) {
|
|
48
|
+
this.rootPath = rootPath;
|
|
49
|
+
}
|
|
50
|
+
file(scopeKey) {
|
|
51
|
+
const digest = crypto.createHash('sha256').update(scopeKey).digest('hex');
|
|
52
|
+
return path.join(this.rootPath, '.newmark-context-v2', 'compression-history', `${digest}.jsonl`);
|
|
53
|
+
}
|
|
54
|
+
append(scopeKey, event) {
|
|
55
|
+
const file = this.file(scopeKey);
|
|
56
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
57
|
+
fs.appendFileSync(file, `${JSON.stringify(event)}\n`, 'utf-8');
|
|
58
|
+
}
|
|
59
|
+
archive(scopeKey, entry) {
|
|
60
|
+
this.append(scopeKey, {
|
|
61
|
+
version: 1,
|
|
62
|
+
type: 'fold',
|
|
63
|
+
at: new Date().toISOString(),
|
|
64
|
+
entry: { ...entry, messages: entry.messages.map(message => ({ ...message })) },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
markRestored(scopeKey, id) {
|
|
68
|
+
this.append(scopeKey, { version: 1, type: 'restore', at: new Date().toISOString(), id });
|
|
69
|
+
}
|
|
70
|
+
/** Replays the append-only ledger and returns only currently restorable entries. */
|
|
71
|
+
activeEntries(scopeKey) {
|
|
72
|
+
const file = this.file(scopeKey);
|
|
73
|
+
if (!fs.existsSync(file))
|
|
74
|
+
return [];
|
|
75
|
+
const active = new Map();
|
|
76
|
+
const lines = fs.readFileSync(file, 'utf-8').split(/\r?\n/);
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
if (!line.trim())
|
|
79
|
+
continue;
|
|
80
|
+
try {
|
|
81
|
+
const event = JSON.parse(line);
|
|
82
|
+
if (event.version !== 1)
|
|
83
|
+
continue;
|
|
84
|
+
if (event.type === 'restore') {
|
|
85
|
+
active.delete(String(event.id || ''));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (event.type !== 'fold' || !event.entry || typeof event.entry !== 'object')
|
|
89
|
+
continue;
|
|
90
|
+
const candidate = event.entry;
|
|
91
|
+
if (!candidate.id || !Array.isArray(candidate.messages) || typeof candidate.summary !== 'string')
|
|
92
|
+
continue;
|
|
93
|
+
active.set(candidate.id, {
|
|
94
|
+
...candidate,
|
|
95
|
+
messages: candidate.messages.map(message => ({ ...message })),
|
|
96
|
+
foldedEntries: Math.max(0, Number(candidate.foldedEntries) || candidate.messages.length),
|
|
97
|
+
foldedChars: Math.max(0, Number(candidate.foldedChars) || 0),
|
|
98
|
+
model: String(candidate.model || 'unknown'),
|
|
99
|
+
fallback: Boolean(candidate.fallback),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// A partial/corrupt line must not hide earlier valid append-only events.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return [...active.values()];
|
|
107
|
+
}
|
|
108
|
+
/** Includes restored/tombstoned ids so a restart never reuses an archive id. */
|
|
109
|
+
maxNumericId(scopeKey) {
|
|
110
|
+
const file = this.file(scopeKey);
|
|
111
|
+
if (!fs.existsSync(file))
|
|
112
|
+
return 0;
|
|
113
|
+
let max = 0;
|
|
114
|
+
for (const line of fs.readFileSync(file, 'utf-8').split(/\r?\n/)) {
|
|
115
|
+
if (!line.trim())
|
|
116
|
+
continue;
|
|
117
|
+
try {
|
|
118
|
+
const event = JSON.parse(line);
|
|
119
|
+
const entry = event.entry && typeof event.entry === 'object' ? event.entry : null;
|
|
120
|
+
const id = String(entry?.id || event.id || '');
|
|
121
|
+
max = Math.max(max, Number(id.replace(/^ctx-cache-/, '')) || 0);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Ignore only the malformed line; valid earlier ids remain authoritative.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return max;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.CompressionHistoryArchive = CompressionHistoryArchive;
|
|
131
|
+
//# sourceMappingURL=compressionHistoryArchive.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide Computer-Use session state.
|
|
3
|
+
*
|
|
4
|
+
* The state is keyed by conversation runtime, not by Build/run id. A Build may finish
|
|
5
|
+
* or be interrupted/replaced while the conversation keeps its
|
|
6
|
+
* Computer-Use switch and lease. Only an explicit stop/toggle-off or runtime
|
|
7
|
+
* teardown releases it.
|
|
8
|
+
*/
|
|
9
|
+
export interface ComputerUseSessionScope {
|
|
10
|
+
runtimeKey: string;
|
|
11
|
+
ownerLabel: string;
|
|
12
|
+
workspacePath?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ComputerUseSessionState {
|
|
15
|
+
runtimeKey: string;
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
occupied: boolean;
|
|
18
|
+
ownerLabel?: string;
|
|
19
|
+
updatedAt?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const COMPUTER_USE_OCCUPIED_MARKER = "computerUse occupied";
|
|
22
|
+
export declare const COMPUTER_USE_LOCK_TTL_MS: number;
|
|
23
|
+
export declare class ComputerUseSessionRegistry {
|
|
24
|
+
private readonly ttlMs;
|
|
25
|
+
private readonly enabledByRuntime;
|
|
26
|
+
private activeLease;
|
|
27
|
+
constructor(ttlMs?: number);
|
|
28
|
+
authorize(action: string, scope: ComputerUseSessionScope, dryRun?: boolean): string | null;
|
|
29
|
+
complete(action: string, scope: ComputerUseSessionScope): void;
|
|
30
|
+
setEnabled(scope: ComputerUseSessionScope, enabled: boolean): {
|
|
31
|
+
ok: true;
|
|
32
|
+
state: ComputerUseSessionState;
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
error: string;
|
|
36
|
+
state: ComputerUseSessionState;
|
|
37
|
+
};
|
|
38
|
+
state(runtimeKey: string): ComputerUseSessionState;
|
|
39
|
+
cancelTarget(runtimeKey: string): boolean;
|
|
40
|
+
private clearExpired;
|
|
41
|
+
private occupiedError;
|
|
42
|
+
}
|
|
43
|
+
export declare const defaultComputerUseSessionRegistry: ComputerUseSessionRegistry;
|
|
44
|
+
//# sourceMappingURL=computerUseSession.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultComputerUseSessionRegistry = exports.ComputerUseSessionRegistry = exports.COMPUTER_USE_LOCK_TTL_MS = exports.COMPUTER_USE_OCCUPIED_MARKER = void 0;
|
|
4
|
+
exports.COMPUTER_USE_OCCUPIED_MARKER = 'computerUse occupied';
|
|
5
|
+
exports.COMPUTER_USE_LOCK_TTL_MS = 10 * 60 * 1000;
|
|
6
|
+
class ComputerUseSessionRegistry {
|
|
7
|
+
ttlMs;
|
|
8
|
+
enabledByRuntime = new Map();
|
|
9
|
+
activeLease = null;
|
|
10
|
+
constructor(ttlMs = exports.COMPUTER_USE_LOCK_TTL_MS) {
|
|
11
|
+
this.ttlMs = ttlMs;
|
|
12
|
+
}
|
|
13
|
+
authorize(action, scope, dryRun = false) {
|
|
14
|
+
const normalizedAction = String(action || '').trim().toLowerCase();
|
|
15
|
+
const runtimeKey = String(scope.runtimeKey || '').trim() || 'conversation:default';
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
this.clearExpired(now);
|
|
18
|
+
if (this.activeLease && this.activeLease.runtimeKey !== runtimeKey) {
|
|
19
|
+
return this.occupiedError(normalizedAction, scope.ownerLabel, this.activeLease.ownerLabel);
|
|
20
|
+
}
|
|
21
|
+
if (normalizedAction === 'takeover_stop')
|
|
22
|
+
return null;
|
|
23
|
+
const enabled = this.enabledByRuntime.get(runtimeKey) !== false;
|
|
24
|
+
const readOnly = normalizedAction === 'observe' || normalizedAction === 'app_list' || normalizedAction === 'app_observe' || normalizedAction === 'wait';
|
|
25
|
+
if (!enabled && !readOnly && !dryRun && normalizedAction !== 'takeover_start') {
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
ok: false,
|
|
28
|
+
action: normalizedAction,
|
|
29
|
+
error: `ComputerUse is disabled for ${scope.ownerLabel}. Enable ComputerUse for this conversation before sending desktop operations.`,
|
|
30
|
+
computer_use_enabled: false,
|
|
31
|
+
requested_owner: scope.ownerLabel,
|
|
32
|
+
}, null, 2);
|
|
33
|
+
}
|
|
34
|
+
if (normalizedAction === 'takeover_start')
|
|
35
|
+
this.enabledByRuntime.set(runtimeKey, true);
|
|
36
|
+
if (!this.activeLease) {
|
|
37
|
+
this.activeLease = { ...scope, runtimeKey, updatedAt: now };
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.activeLease.updatedAt = now;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
complete(action, scope) {
|
|
45
|
+
const normalizedAction = String(action || '').trim().toLowerCase();
|
|
46
|
+
const runtimeKey = String(scope.runtimeKey || '').trim() || 'conversation:default';
|
|
47
|
+
if (normalizedAction === 'takeover_stop') {
|
|
48
|
+
if (!this.activeLease || this.activeLease.runtimeKey === runtimeKey)
|
|
49
|
+
this.activeLease = null;
|
|
50
|
+
this.enabledByRuntime.set(runtimeKey, false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (this.activeLease?.runtimeKey === runtimeKey)
|
|
54
|
+
this.activeLease.updatedAt = Date.now();
|
|
55
|
+
}
|
|
56
|
+
setEnabled(scope, enabled) {
|
|
57
|
+
const runtimeKey = String(scope.runtimeKey || '').trim() || 'conversation:default';
|
|
58
|
+
this.clearExpired();
|
|
59
|
+
if (this.activeLease && this.activeLease.runtimeKey !== runtimeKey) {
|
|
60
|
+
return { ok: false, error: this.occupiedError('toggle', scope.ownerLabel, this.activeLease.ownerLabel), state: this.state(runtimeKey) };
|
|
61
|
+
}
|
|
62
|
+
this.enabledByRuntime.set(runtimeKey, enabled !== false);
|
|
63
|
+
if (enabled === false && this.activeLease?.runtimeKey === runtimeKey)
|
|
64
|
+
this.activeLease = null;
|
|
65
|
+
return { ok: true, state: this.state(runtimeKey) };
|
|
66
|
+
}
|
|
67
|
+
state(runtimeKey) {
|
|
68
|
+
const key = String(runtimeKey || '').trim() || 'conversation:default';
|
|
69
|
+
this.clearExpired();
|
|
70
|
+
const lease = this.activeLease;
|
|
71
|
+
return {
|
|
72
|
+
runtimeKey: key,
|
|
73
|
+
enabled: this.enabledByRuntime.get(key) !== false,
|
|
74
|
+
occupied: !!lease,
|
|
75
|
+
...(lease ? { ownerLabel: lease.ownerLabel, updatedAt: lease.updatedAt } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
cancelTarget(runtimeKey) {
|
|
79
|
+
const key = String(runtimeKey || '').trim();
|
|
80
|
+
if (!key)
|
|
81
|
+
return false;
|
|
82
|
+
const hadActiveLease = this.activeLease?.runtimeKey === key;
|
|
83
|
+
if (hadActiveLease)
|
|
84
|
+
this.activeLease = null;
|
|
85
|
+
this.enabledByRuntime.set(key, false);
|
|
86
|
+
return hadActiveLease;
|
|
87
|
+
}
|
|
88
|
+
clearExpired(now = Date.now()) {
|
|
89
|
+
if (this.activeLease && now - this.activeLease.updatedAt > this.ttlMs) {
|
|
90
|
+
this.activeLease = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
occupiedError(action, requestedOwner, activeOwner) {
|
|
94
|
+
return JSON.stringify({
|
|
95
|
+
ok: false,
|
|
96
|
+
action,
|
|
97
|
+
error: `${exports.COMPUTER_USE_OCCUPIED_MARKER}: ComputerUse is already active in ${activeOwner}. Stop it with computer_use takeover_stop or wait before another conversation takes control.`,
|
|
98
|
+
lock_owner: activeOwner,
|
|
99
|
+
requested_owner: requestedOwner,
|
|
100
|
+
}, null, 2);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.ComputerUseSessionRegistry = ComputerUseSessionRegistry;
|
|
104
|
+
exports.defaultComputerUseSessionRegistry = new ComputerUseSessionRegistry();
|
|
105
|
+
//# sourceMappingURL=computerUseSession.js.map
|
package/dist/core/config.js
CHANGED
|
@@ -913,6 +913,7 @@ function defaultConfig() {
|
|
|
913
913
|
keep_recent_messages: { _description: "Keep recent messages", _type: "integer", value: 10 },
|
|
914
914
|
preserve_recent_messages: { _description: "dev-0.3.8 protected recent-message zone for context_history_manage (0 disables)", _type: "integer", value: 5 },
|
|
915
915
|
compression_cache_max: { _description: "dev-0.3.8 max folded-segment cache entries retained for restore/search", _type: "integer", value: 8 },
|
|
916
|
+
compression_archive_enabled: { _description: "dev-0.3.9 append-only cold archive for folded segments evicted from the hot cache", _type: "boolean", value: true },
|
|
916
917
|
structured_context_v2: { _description: "dev-0.3.0 structured context v2 (orchestrator + fixed order + snapshot)", _type: "boolean", value: true },
|
|
917
918
|
build_history_persistence: { _description: "dev-0.3.0 append-only Build History persistence", _type: "boolean", value: true },
|
|
918
919
|
branch_log_v2: { _description: "dev-0.3.0 branch long-log v2 (epoch summaries)", _type: "boolean", value: true },
|
|
@@ -198,7 +198,7 @@ export declare class ConversationKernel {
|
|
|
198
198
|
setWorkRunExpanded(target: ConversationTargetInput, runId: string, expanded: boolean): boolean;
|
|
199
199
|
setInputMode(target: ConversationTargetInput, mode: string): 'guide' | 'next';
|
|
200
200
|
setMode(target: ConversationTargetInput, mode: AgentMode): AgentMode;
|
|
201
|
-
toggleGoalPause(target: ConversationTargetInput): boolean
|
|
201
|
+
toggleGoalPause(target: ConversationTargetInput): Promise<boolean>;
|
|
202
202
|
clearGoal(target: ConversationTargetInput): boolean;
|
|
203
203
|
updateSetting(section: string, key: string, value: unknown): void;
|
|
204
204
|
runtimeState(target: ConversationTargetInput): ConversationRuntimeState | null;
|
|
@@ -212,6 +212,8 @@ export declare class ConversationKernel {
|
|
|
212
212
|
private processTimeoutMs;
|
|
213
213
|
private runtime;
|
|
214
214
|
private scheduleGoalContinuation;
|
|
215
|
+
private schedulePendingRuntimeContinuation;
|
|
216
|
+
private startGoalDrivenBuild;
|
|
215
217
|
private createRunner;
|
|
216
218
|
private enqueueRootInboxWake;
|
|
217
219
|
private applyOptions;
|
|
@@ -217,6 +217,7 @@ class ConversationKernel {
|
|
|
217
217
|
runtime.runner.recordGuideReceipt(deferred);
|
|
218
218
|
this.emitQueueUpdate(runtime);
|
|
219
219
|
this.activateAcceptedGoal(runtime, envelope.goalObjective);
|
|
220
|
+
this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
|
|
220
221
|
return deferred;
|
|
221
222
|
}
|
|
222
223
|
if (runtime.guideAcceptanceClosedRunId === runtime.runId) {
|
|
@@ -248,6 +249,7 @@ class ConversationKernel {
|
|
|
248
249
|
createdAt: deferred.createdAt,
|
|
249
250
|
}]);
|
|
250
251
|
this.activateAcceptedGoal(runtime, envelope.goalObjective);
|
|
252
|
+
this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
|
|
251
253
|
return deferred;
|
|
252
254
|
}
|
|
253
255
|
const queued = runtime.runner.queueActiveKernelMessage(safeEnvelope.text, 'steer', clientMessageId, runtime.runId, safeEnvelope.images);
|
|
@@ -278,6 +280,7 @@ class ConversationKernel {
|
|
|
278
280
|
createdAt: deferred.createdAt,
|
|
279
281
|
}]);
|
|
280
282
|
this.activateAcceptedGoal(runtime, envelope.goalObjective);
|
|
283
|
+
this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
|
|
281
284
|
return deferred;
|
|
282
285
|
}
|
|
283
286
|
checkpoint(target) {
|
|
@@ -333,11 +336,26 @@ class ConversationKernel {
|
|
|
333
336
|
runtime.options.mode = mode;
|
|
334
337
|
return runner.mode;
|
|
335
338
|
}
|
|
336
|
-
toggleGoalPause(target) {
|
|
339
|
+
async toggleGoalPause(target) {
|
|
337
340
|
const normalized = this.normalizeTarget(target);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
+
let runtime = this.findRuntime(normalized);
|
|
342
|
+
if (!runtime) {
|
|
343
|
+
const runner = this.createRunner(normalized);
|
|
344
|
+
runtime = this.runtime(normalized, {
|
|
345
|
+
mode: runner.mode,
|
|
346
|
+
model: runner.model,
|
|
347
|
+
intelligence: runner.intelligence,
|
|
348
|
+
inputMode: runner.inputMode,
|
|
349
|
+
engine: runner.engine,
|
|
350
|
+
}, runner);
|
|
351
|
+
}
|
|
352
|
+
const wasPaused = runtime.runner.isGoalPaused();
|
|
353
|
+
const hadGoal = !!runtime.runner.goal;
|
|
354
|
+
const paused = runtime.runner.toggleGoalPause();
|
|
355
|
+
if (hadGoal && wasPaused && !paused)
|
|
356
|
+
this.startGoalDrivenBuild(runtime);
|
|
357
|
+
this.mirrorHostIfTargetActive(runtime);
|
|
358
|
+
return paused;
|
|
341
359
|
}
|
|
342
360
|
clearGoal(target) {
|
|
343
361
|
const normalized = this.normalizeTarget(target);
|
|
@@ -401,6 +419,7 @@ class ConversationKernel {
|
|
|
401
419
|
}
|
|
402
420
|
runtime.stopRequestedRunId = runtime.runId;
|
|
403
421
|
runtime.forceStopArmedRunId = runtime.runId;
|
|
422
|
+
runtime.runner.pauseGoalForUserInterrupt();
|
|
404
423
|
this.retainUnconsumedKernelMessages(runtime);
|
|
405
424
|
this.deferOutstandingGuides(runtime);
|
|
406
425
|
const checkpointed = this.checkpoint(runtime.target).checkpointed;
|
|
@@ -491,6 +510,12 @@ class ConversationKernel {
|
|
|
491
510
|
stopped = true;
|
|
492
511
|
this.settleCooperativeStop(runtime, runId);
|
|
493
512
|
}
|
|
513
|
+
else if (runtime.pendingNextTurn.length > 0) {
|
|
514
|
+
// A renderer/IPC Guide can arrive after the final-drain barrier's
|
|
515
|
+
// last check but before this promise settles. Do not leave the
|
|
516
|
+
// deferred continuation queued on an idle runtime.
|
|
517
|
+
this.schedulePendingRuntimeContinuation(runtime, runId);
|
|
518
|
+
}
|
|
494
519
|
}
|
|
495
520
|
}
|
|
496
521
|
if (!stopped && runtime.runId === runId)
|
|
@@ -610,7 +635,7 @@ class ConversationKernel {
|
|
|
610
635
|
return 0;
|
|
611
636
|
return Math.max(1000, Math.floor(raw));
|
|
612
637
|
}
|
|
613
|
-
runtime(target, options) {
|
|
638
|
+
runtime(target, options, runnerOverride) {
|
|
614
639
|
const existing = this.findRuntime(target);
|
|
615
640
|
if (existing) {
|
|
616
641
|
this.ensureRuntimeMetadata(existing, target);
|
|
@@ -618,7 +643,7 @@ class ConversationKernel {
|
|
|
618
643
|
return existing;
|
|
619
644
|
}
|
|
620
645
|
const id = target.conversationId;
|
|
621
|
-
const runner = this.createRunner(target);
|
|
646
|
+
const runner = runnerOverride || this.createRunner(target);
|
|
622
647
|
runner.setAutomationManager(this.automation);
|
|
623
648
|
this.applyOptions(runner, options);
|
|
624
649
|
const runtime = {
|
|
@@ -640,6 +665,7 @@ class ConversationKernel {
|
|
|
640
665
|
guideReceipts: new Map(),
|
|
641
666
|
guideEnvelopes: new Map(),
|
|
642
667
|
goalContinuationTimer: undefined,
|
|
668
|
+
pendingContinuationRunId: undefined,
|
|
643
669
|
};
|
|
644
670
|
runner.setGoalContinuationGate(() => {
|
|
645
671
|
this.queueState(runtime);
|
|
@@ -732,8 +758,51 @@ class ConversationKernel {
|
|
|
732
758
|
});
|
|
733
759
|
}, 250);
|
|
734
760
|
}
|
|
761
|
+
schedulePendingRuntimeContinuation(runtime, runId) {
|
|
762
|
+
if (runtime.pendingContinuationRunId === runId)
|
|
763
|
+
return;
|
|
764
|
+
runtime.pendingContinuationRunId = runId;
|
|
765
|
+
const active = runtime.activePromise;
|
|
766
|
+
if (active) {
|
|
767
|
+
const continueAfterSettlement = () => {
|
|
768
|
+
runtime.pendingContinuationRunId = undefined;
|
|
769
|
+
this.schedulePendingRuntimeContinuation(runtime, runId);
|
|
770
|
+
};
|
|
771
|
+
void active.then(continueAfterSettlement, continueAfterSettlement);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
setImmediate(() => {
|
|
775
|
+
runtime.pendingContinuationRunId = undefined;
|
|
776
|
+
if (runtime.runId !== runId || runtime.activePromise || runtime.stopRequestedRunId === runId)
|
|
777
|
+
return;
|
|
778
|
+
const next = runtime.pendingNextTurn.shift();
|
|
779
|
+
if (!next)
|
|
780
|
+
return;
|
|
781
|
+
const message = typeof next.message === 'string'
|
|
782
|
+
? { text: next.message, runId }
|
|
783
|
+
: { ...next.message, runId: next.message.runId || runId };
|
|
784
|
+
void this.prompt(message, runtime.target, runtime.options, next.queueMode).catch(() => {
|
|
785
|
+
// Agent.process and the work-run finalizer already publish the error.
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
startGoalDrivenBuild(runtime) {
|
|
790
|
+
if (runtime.goalContinuationTimer) {
|
|
791
|
+
clearTimeout(runtime.goalContinuationTimer);
|
|
792
|
+
runtime.goalContinuationTimer = undefined;
|
|
793
|
+
}
|
|
794
|
+
const message = runtime.runner.claimGoalContinuationMessage({ force: true });
|
|
795
|
+
if (!message)
|
|
796
|
+
return;
|
|
797
|
+
void this.prompt(message, runtime.target, { ...runtime.options, mode: 'goal' }, 'followUp').catch(() => {
|
|
798
|
+
// Agent.process records and publishes the Build error.
|
|
799
|
+
});
|
|
800
|
+
}
|
|
735
801
|
createRunner(target) {
|
|
736
|
-
const runner = this.lifecycle.createRunner?.(target) || new agent_1.Agent(this.root, {
|
|
802
|
+
const runner = this.lifecycle.createRunner?.(target) || new agent_1.Agent(this.root, {
|
|
803
|
+
actorId: this.host.runtimeActorId,
|
|
804
|
+
runtimeLifecycleRole: this.host.runtimeLifecycleRole,
|
|
805
|
+
});
|
|
737
806
|
if (target.workspace) {
|
|
738
807
|
runner.workspace.current = {
|
|
739
808
|
id: target.workspace.id,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ElectronBrowserUseHost = void 0;
|
|
4
|
+
const browserUsePageAdapter_1 = require("./browserUsePageAdapter");
|
|
4
5
|
const SAFE_NAVIGATION = /^(?:https?:|about:blank|newmark-preview:)/i;
|
|
5
6
|
const BROWSER_USE_WORLD_ID = 999;
|
|
6
7
|
/**
|
|
@@ -94,12 +95,27 @@ class ElectronBrowserUseHost {
|
|
|
94
95
|
evaluateFixed: async (script, signal) => await raceWithAbort(contents.executeJavaScriptInIsolatedWorld(BROWSER_USE_WORLD_ID, [{ code: script }], true), signal),
|
|
95
96
|
clickAt: async (x, y, signal) => {
|
|
96
97
|
throwIfAborted(signal);
|
|
98
|
+
// A webview guest can retain the DOM focus while its embedder is still
|
|
99
|
+
// committing a workspace/tab transition. Focus both sides and yield
|
|
100
|
+
// between native input events so Chromium receives a real click rather
|
|
101
|
+
// than a synchronously queued sequence that can be dropped by the guest.
|
|
102
|
+
contents.hostWebContents?.focus();
|
|
97
103
|
contents.focus();
|
|
104
|
+
await abortableDelay(10, signal);
|
|
98
105
|
const point = { x: Math.max(0, Math.round(x)), y: Math.max(0, Math.round(y)) };
|
|
99
106
|
contents.sendInputEvent({ type: 'mouseMove', ...point });
|
|
107
|
+
await abortableDelay(10, signal);
|
|
100
108
|
contents.sendInputEvent({ type: 'mouseDown', button: 'left', clickCount: 1, ...point });
|
|
109
|
+
await abortableDelay(10, signal);
|
|
101
110
|
contents.sendInputEvent({ type: 'mouseUp', button: 'left', clickCount: 1, ...point });
|
|
111
|
+
await abortableDelay(10, signal);
|
|
102
112
|
},
|
|
113
|
+
clickElement: contents.getType() === 'webview' ? async (token, signal) => {
|
|
114
|
+
throwIfAborted(signal);
|
|
115
|
+
const result = await raceWithAbort(contents.executeJavaScriptInIsolatedWorld(BROWSER_USE_WORLD_ID, [{ code: (0, browserUsePageAdapter_1.browserUseClickScript)(token) }], true), signal);
|
|
116
|
+
if (!result?.clicked)
|
|
117
|
+
throw new Error(result?.error || 'Unable to click the observed Browser-Use element.');
|
|
118
|
+
} : undefined,
|
|
103
119
|
replaceFocusedText: async (text, signal) => {
|
|
104
120
|
throwIfAborted(signal);
|
|
105
121
|
contents.focus();
|