@kingsoftcloud/agentkit-harness-node 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +1 -0
- package/README.md +124 -2
- package/THIRD_PARTY_NOTICES.md +66 -0
- package/ZCode-LICENSE +202 -0
- package/dist/index.mjs +4871 -0
- package/dist/native-command-plan.LICENSE.txt +221 -0
- package/dist/native-command-plan.json +16 -0
- package/dist/native-command-plan.mjs +8 -0
- package/dist/rpc-entry.mjs +5682 -0
- package/package.json +169 -4
- package/src/native/a2a-receipt-source.mjs +19 -0
- package/src/native/a2a-tools.mjs +517 -0
- package/src/native/activity-status.mjs +30 -0
- package/src/native/artifact-codec.mjs +481 -0
- package/src/native/artifact-store.mjs +1162 -0
- package/src/native/assistant-stream.mjs +76 -0
- package/src/native/background-command-port.mjs +95 -0
- package/src/native/browser-action-observation.mjs +43 -0
- package/src/native/browser-availability.mjs +22 -0
- package/src/native/browser-canvas-metrics.mjs +71 -0
- package/src/native/browser-dom.mjs +193 -0
- package/src/native/browser-playwright.mjs +1249 -0
- package/src/native/browser-vision.mjs +369 -0
- package/src/native/browser-workspace-policy.mjs +22 -0
- package/src/native/browser-workspace-protocol.mjs +95 -0
- package/src/native/browser-workspace-server.mjs +164 -0
- package/src/native/browser-workspace.mjs +355 -0
- package/src/native/captured-source.mjs +3 -0
- package/src/native/child-budget-allocation.mjs +87 -0
- package/src/native/child-capabilities.mjs +35 -0
- package/src/native/child-command-resource.mjs +20 -0
- package/src/native/child-history.mjs +188 -0
- package/src/native/child-merge.mjs +98 -0
- package/src/native/child-model-surface.mjs +19 -0
- package/src/native/child-observation.mjs +100 -0
- package/src/native/child-owner.mjs +620 -0
- package/src/native/child-publication.mjs +76 -0
- package/src/native/child-readers.mjs +366 -0
- package/src/native/child-tools.mjs +46 -0
- package/src/native/child-workspace-binding.mjs +29 -0
- package/src/native/child-workspace-tools.mjs +1 -0
- package/src/native/child-workspaces.mjs +655 -0
- package/src/native/children.mjs +365 -0
- package/src/native/command-plan-worker.mjs +14 -0
- package/src/native/command-plan.mjs +31 -0
- package/src/native/command-progress.mjs +64 -0
- package/src/native/command-shell.mjs +9 -0
- package/src/native/command-workspace-observation.mjs +36 -0
- package/src/native/component-tools.mjs +280 -0
- package/src/native/context-counter.mjs +133 -0
- package/src/native/context-markers.mjs +51 -0
- package/src/native/context-profile.mjs +72 -0
- package/src/native/context-status.mjs +27 -0
- package/src/native/conversation-command-control.mjs +133 -0
- package/src/native/delivery-timeline.mjs +39 -0
- package/src/native/dsh-background-ownership.mjs +102 -0
- package/src/native/dsh-managed-ownership.mjs +23 -0
- package/src/native/dsh-turn-contract.mjs +3 -0
- package/src/native/dsh-turn-ownership.mjs +54 -0
- package/src/native/dsh-turn-runtime.mjs +498 -0
- package/src/native/e2b-background-supervisor.mjs +347 -0
- package/src/native/e2b-background.mjs +368 -0
- package/src/native/e2b-command-state.mjs +40 -0
- package/src/native/e2b-command-tools.mjs +486 -0
- package/src/native/e2b-sandbox-owner.mjs +227 -0
- package/src/native/hook-executor.mjs +249 -0
- package/src/native/instruction-assembly.mjs +60 -0
- package/src/native/instruction-context.mjs +54 -0
- package/src/native/local-background-supervisor.mjs +108 -0
- package/src/native/local-background.mjs +207 -0
- package/src/native/local-command-evidence.mjs +57 -0
- package/src/native/local-command-policy-worker.mjs +197 -0
- package/src/native/local-command-policy.mjs +46 -0
- package/src/native/local-command-state.mjs +177 -0
- package/src/native/local-command-tools.mjs +302 -0
- package/src/native/local-execution-profile.mjs +36 -0
- package/src/native/local-output-policy.mjs +92 -0
- package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
- package/src/native/local-process-bin/darwin-x64/launcher +0 -0
- package/src/native/local-process-bin/linux-arm64/launcher +0 -0
- package/src/native/local-process-bin/linux-x64/launcher +0 -0
- package/src/native/local-process-launcher.c +183 -0
- package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
- package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
- package/src/native/local-process-licenses/NOTICE +21 -0
- package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
- package/src/native/local-process-manifest.json +32 -0
- package/src/native/local-process.mjs +246 -0
- package/src/native/local-resource-profile.mjs +32 -0
- package/src/native/memory-candidate.mjs +121 -0
- package/src/native/memory-extraction-run.mjs +60 -0
- package/src/native/memory-extraction-tools.mjs +116 -0
- package/src/native/memory-recall.mjs +65 -0
- package/src/native/memory-store.mjs +334 -0
- package/src/native/memory-tools.mjs +329 -0
- package/src/native/model-anthropic-cache.mjs +49 -0
- package/src/native/model-anthropic-tool-search.mjs +85 -0
- package/src/native/model-anthropic.mjs +753 -0
- package/src/native/model-chat-usage.mjs +24 -0
- package/src/native/model-inline-reasoning.mjs +111 -0
- package/src/native/model-media.mjs +174 -0
- package/src/native/model-normalize.mjs +228 -0
- package/src/native/model-openai-chat.mjs +561 -0
- package/src/native/model-port.mjs +361 -0
- package/src/native/model-provider-compat.mjs +129 -0
- package/src/native/model-replay.mjs +153 -0
- package/src/native/model-request.mjs +15 -0
- package/src/native/model-responses.mjs +1205 -0
- package/src/native/model-sse.mjs +171 -0
- package/src/native/model-surface-host.mjs +27 -0
- package/src/native/model-tool-search-policy.mjs +16 -0
- package/src/native/model-transport-observation.mjs +27 -0
- package/src/native/owned-child.mjs +114 -0
- package/src/native/owned-command.mjs +106 -0
- package/src/native/python-syntax-worker.mjs +60 -0
- package/src/native/python-syntax.mjs +50 -0
- package/src/native/read-interruption.mjs +16 -0
- package/src/native/result-stream.mjs +63 -0
- package/src/native/ruff-ast.mjs +95 -0
- package/src/native/run-observation.mjs +128 -0
- package/src/native/session-delivery-journal.mjs +96 -0
- package/src/native/session-delivery.mjs +434 -0
- package/src/native/shell-analysis.mjs +50 -0
- package/src/native/skill-disclosure.mjs +404 -0
- package/src/native/skill-events.mjs +164 -0
- package/src/native/skill-tools.mjs +813 -0
- package/src/native/source-page.mjs +73 -0
- package/src/native/sqlite-session-inbox.mjs +42 -0
- package/src/native/sqlite-store.mjs +617 -0
- package/src/native/tool-authority.mjs +99 -0
- package/src/native/tool-guidance.mjs +12 -0
- package/src/native/tool-observation.mjs +75 -0
- package/src/native/tool-states.mjs +14 -0
- package/src/native/verification-authority.mjs +37 -0
- package/src/native/web-tools.mjs +643 -0
- package/src/native/workspace-checkpoints.mjs +312 -0
- package/src/native/workspace-context.mjs +61 -0
- package/src/native/workspace-display.mjs +39 -0
- package/src/native/workspace-edit-match.mjs +19 -0
- package/src/native/workspace-edit-worker.mjs +15 -0
- package/src/native/workspace-editing.mjs +447 -0
- package/src/native/workspace-image.mjs +72 -0
- package/src/native/workspace-instructions.mjs +107 -0
- package/src/native/workspace-mutation-lock.mjs +74 -0
- package/src/native/workspace-patch-worker.mjs +17 -0
- package/src/native/workspace-patch.mjs +17 -0
- package/src/native/workspace-paths.mjs +225 -0
- package/src/native/workspace-state.mjs +272 -0
- package/src/native/workspace-subject-observer.mjs +84 -0
- package/src/native/workspace-tools.mjs +1740 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { invariant } from '@kingsoftcloud/agentkit-harness-core';
|
|
3
|
+
import { finalResultDisplay } from './result-stream.mjs';
|
|
4
|
+
|
|
5
|
+
// Display block identities are local to an attempt. They never authorize a
|
|
6
|
+
// Native tool call, whose canonical identity arrives in the committed message.
|
|
7
|
+
export function nativeStreamProjector(operationKey, configuration) {
|
|
8
|
+
const blocks = new Map();
|
|
9
|
+
const display = finalResultDisplay(configuration);
|
|
10
|
+
const project = record => {
|
|
11
|
+
const { event, time } = record;
|
|
12
|
+
const key = event.kind === 'tool_delta' ? `tool:${event.metadata.index}` : event.kind;
|
|
13
|
+
const output = [];
|
|
14
|
+
if (!blocks.has(key)) {
|
|
15
|
+
const index = blocks.size;
|
|
16
|
+
blocks.set(key, { index, id: `native-preview-${createHash('sha256').update(`${operationKey}:${key}`).digest('hex')}` });
|
|
17
|
+
output.push({ type: 'chunk', time, chunk: { type: 'block-start', index,
|
|
18
|
+
blockType: event.kind === 'text' ? 'text' : event.kind === 'thinking' ? 'reasoning' : 'tool-call' } });
|
|
19
|
+
}
|
|
20
|
+
const { index, id } = blocks.get(key);
|
|
21
|
+
const chunk = event.kind === 'tool_delta'
|
|
22
|
+
? { type: 'tool-call-delta', index, id, argumentsDelta: event.text,
|
|
23
|
+
...(event.metadata.name ? { name: event.metadata.name } : {}) }
|
|
24
|
+
: { type: event.kind === 'text' ? 'text-delta' : 'reasoning-delta', index, text: event.text };
|
|
25
|
+
output.push({ type: 'chunk', time, chunk });
|
|
26
|
+
return output;
|
|
27
|
+
};
|
|
28
|
+
return record => display(record).flatMap(project);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function nativeDurableStream(operationKey, display, configuration) {
|
|
32
|
+
if (display === undefined) return [];
|
|
33
|
+
invariant(display.version === 'agentkit.model-display/v1' && Array.isArray(display.records)
|
|
34
|
+
&& typeof operationKey === 'string', 'native_display_stream_invalid');
|
|
35
|
+
return display.records.flatMap(nativeStreamProjector(operationKey, configuration));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Public DSH frames; publication failures cannot change execution authority. */
|
|
39
|
+
export function nativeLiveStream({ agent, runId, turn, publish, beginStep, revisions, configuration }) {
|
|
40
|
+
const attempts = new Map();
|
|
41
|
+
// DSH continuity revisions count every frame in the Agent lifecycle, not
|
|
42
|
+
// model attempts. Reusing a start revision for its chunks makes the real
|
|
43
|
+
// Session follower discard the active stream as a continuity violation.
|
|
44
|
+
const emit = frame => {
|
|
45
|
+
const revision = (revisions.get(agent) ?? 0) + 1;
|
|
46
|
+
revisions.set(agent, revision);
|
|
47
|
+
try { publish?.(structuredClone({ ...frame, revision })); } catch { /* display observer */ }
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
pending(operationKey) { return attempts.has(operationKey); },
|
|
51
|
+
accept(context, record) {
|
|
52
|
+
if (!publish) return;
|
|
53
|
+
let attempt = attempts.get(context.operationKey);
|
|
54
|
+
if (!attempt) {
|
|
55
|
+
attempt = { attemptId: `${runId}:${context.operationKey}`, index: 0,
|
|
56
|
+
project: nativeStreamProjector(context.operationKey, configuration) };
|
|
57
|
+
attempts.set(context.operationKey, attempt);
|
|
58
|
+
emit({ type: 'start', attemptId: attempt.attemptId, turn, step: beginStep() });
|
|
59
|
+
}
|
|
60
|
+
for (const { time, chunk } of attempt.project(record)) emit({ type: 'chunk', attemptId: attempt.attemptId,
|
|
61
|
+
index: attempt.index++, time, chunk });
|
|
62
|
+
},
|
|
63
|
+
settled(operationKey, eventType, seq) {
|
|
64
|
+
const attempt = attempts.get(operationKey);
|
|
65
|
+
if (!attempt) return;
|
|
66
|
+
attempts.delete(operationKey);
|
|
67
|
+
emit({ type: 'end', attemptId: attempt.attemptId, index: attempt.index,
|
|
68
|
+
outcome: { kind: 'committed', eventType, seq } });
|
|
69
|
+
},
|
|
70
|
+
abandon() {
|
|
71
|
+
for (const attempt of attempts.values()) emit({ type: 'end', attemptId: attempt.attemptId,
|
|
72
|
+
index: attempt.index, outcome: { kind: 'abandoned' } });
|
|
73
|
+
attempts.clear();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Observe existing command handles through the canonical launch receipt. This
|
|
2
|
+
// port has no launch, stop, approval or model capability; those are separate
|
|
3
|
+
// durable tool/Host operations. Backend selection never falls back to the env.
|
|
4
|
+
import { BACKGROUND_COMMAND_CONTRACT, backgroundCommandObservation, backgroundCommandRef,
|
|
5
|
+
commandProbe, canonical, copy, NativeError, BACKGROUND_NOTIFICATION_ADMISSION } from '@kingsoftcloud/agentkit-harness-core';
|
|
6
|
+
import { ownedCommandReceipt } from './owned-command.mjs';
|
|
7
|
+
|
|
8
|
+
const backends = new Set(['local_process', 'e2b']);
|
|
9
|
+
const check = (value, code) => { if (!value) throw new NativeError(code); };
|
|
10
|
+
const same = (left, right) => canonical(left) === canonical(right);
|
|
11
|
+
|
|
12
|
+
/** readers[backend] performs one nonblocking, read-only handle lookup. */
|
|
13
|
+
export function createBackgroundCommandPort({ scope, store, readers = {} }) {
|
|
14
|
+
const pinnedScope = copy(scope), bindings = Object.fromEntries(Object.entries(readers));
|
|
15
|
+
check(Object.entries(bindings).every(([backend, reader]) => backends.has(backend) && typeof reader === 'function'),
|
|
16
|
+
'invalid_background_command_readers');
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
contract: BACKGROUND_COMMAND_CONTRACT,
|
|
19
|
+
/** Controlled idle admission reads the original receipt, never environment
|
|
20
|
+
* hints. This method cannot create a turn or mutate the backend. */
|
|
21
|
+
async observeCompletion(request) {
|
|
22
|
+
const signal = request.signal;
|
|
23
|
+
check(signal instanceof AbortSignal, 'background_notification_signal_invalid');
|
|
24
|
+
const { signal: _, ...body } = request, sealed = canonical(body);
|
|
25
|
+
let original;
|
|
26
|
+
const current = () => {
|
|
27
|
+
signal.throwIfAborted();
|
|
28
|
+
const { signal: liveSignal, ...live } = request;
|
|
29
|
+
check(liveSignal === signal && canonical(live) === sealed && same(request.scope, pinnedScope), 'background_notification_request_drift');
|
|
30
|
+
const source = store.load(pinnedScope, request.sourceRunId), snapshot = source.snapshot;
|
|
31
|
+
const head = store.sessionHead(pinnedScope, snapshot.sessionId);
|
|
32
|
+
const ref = backgroundCommandRef(request.ref);
|
|
33
|
+
check(same(body, { contract: BACKGROUND_NOTIFICATION_ADMISSION, scope: pinnedScope, sourceRunId: snapshot.runId,
|
|
34
|
+
sessionId: snapshot.sessionId, sourceConfigDigest: snapshot.configDigest, sourceRevision: snapshot.revision,
|
|
35
|
+
headRunId: head?.runId, ref }) && snapshot.phase === 'completed' && head?.phase === 'completed'
|
|
36
|
+
&& ref.sourceRunId === snapshot.runId && snapshot.backgroundCommands.some(row => same(row.ref, ref) && !row.notified)
|
|
37
|
+
&& head.backgroundCommands.some(row => same(row.ref, ref) && !row.notified), 'background_notification_read_unbound');
|
|
38
|
+
if (original) check(same(original, source), 'background_notification_read_changed');
|
|
39
|
+
else original = source;
|
|
40
|
+
return { ...ownedCommandReceipt({ scope: pinnedScope, store, current: source, ref }), source };
|
|
41
|
+
};
|
|
42
|
+
const { ref, backend, source } = current();
|
|
43
|
+
if (!backend || !bindings[backend]) return backgroundCommandObservation({ ref, status: 'unavailable' });
|
|
44
|
+
let output;
|
|
45
|
+
try {
|
|
46
|
+
output = await bindings[backend]({ scope: copy(pinnedScope), sessionId: source.snapshot.sessionId,
|
|
47
|
+
runId: source.snapshot.runId, ref: copy(ref), stdoutCursor: 0, stderrCursor: 0, maxBytes: 8192, waitSeconds: 0, signal });
|
|
48
|
+
} catch {
|
|
49
|
+
current(); return backgroundCommandObservation({ ref, status: 'unavailable' });
|
|
50
|
+
}
|
|
51
|
+
current();
|
|
52
|
+
if (['starting', 'running'].includes(output?.status)) return null;
|
|
53
|
+
try {
|
|
54
|
+
check(output && typeof output === 'object' && !Array.isArray(output), 'invalid_background_command_output');
|
|
55
|
+
return backgroundCommandObservation({ ...output, ref,
|
|
56
|
+
outputMayBeTruncated: output.outputMayBeTruncated === true || output.stdoutCursor >= 8192 || output.stderrCursor >= 8192 });
|
|
57
|
+
} catch { return backgroundCommandObservation({ ref, status: 'unavailable' }); }
|
|
58
|
+
},
|
|
59
|
+
async observe(request) {
|
|
60
|
+
request.signal.throwIfAborted();
|
|
61
|
+
check(same(request.scope, pinnedScope), 'background_command_scope_mismatch');
|
|
62
|
+
const current = store.load(pinnedScope, request.runId);
|
|
63
|
+
const config = current.snapshot.taskContract ? { ...current.configuration, contract: current.snapshot.taskContract } : current.configuration;
|
|
64
|
+
const intent = current.intents.find(item => item.operationKey === current.snapshot.pendingIntent) ?? null;
|
|
65
|
+
const expected = commandProbe(current.snapshot, config, intent), { signal, ...plain } = request;
|
|
66
|
+
check(expected && same(expected, plain), 'background_command_owner_mismatch');
|
|
67
|
+
check(new Set(request.refs.map(ref => ref.taskId)).size === request.refs.length, 'background_command_owner_mismatch');
|
|
68
|
+
// Verify the entire request before touching any external handle.
|
|
69
|
+
const owned = request.refs.map(candidate => {
|
|
70
|
+
return ownedCommandReceipt({ scope: pinnedScope, store, current, ref: backgroundCommandRef(candidate) });
|
|
71
|
+
});
|
|
72
|
+
const observations = [];
|
|
73
|
+
for (const { ref, backend } of owned) {
|
|
74
|
+
signal.throwIfAborted();
|
|
75
|
+
const unavailable = () => backgroundCommandObservation({ ref, status: 'unavailable' });
|
|
76
|
+
if (!backend || !bindings[backend]) observations.push(unavailable());
|
|
77
|
+
else {
|
|
78
|
+
try {
|
|
79
|
+
const output = await bindings[backend]({ scope: copy(pinnedScope), sessionId: current.snapshot.sessionId,
|
|
80
|
+
runId: request.runId, ref: copy(ref), stdoutCursor: 0, stderrCursor: 0, maxBytes: 8192, waitSeconds: 0, signal });
|
|
81
|
+
signal.throwIfAborted();
|
|
82
|
+
check(output && typeof output === 'object' && !Array.isArray(output), 'invalid_background_command_output');
|
|
83
|
+
check(output.outputMayBeTruncated === undefined || typeof output.outputMayBeTruncated === 'boolean', 'invalid_background_command_output');
|
|
84
|
+
if (['starting', 'running'].includes(output.status)) continue;
|
|
85
|
+
observations.push(backgroundCommandObservation({ ...output, ref,
|
|
86
|
+
outputMayBeTruncated: output.outputMayBeTruncated === true || output.stdoutCursor >= 8192 || output.stderrCursor >= 8192,
|
|
87
|
+
}));
|
|
88
|
+
} catch (error) { signal.throwIfAborted(); observations.push(unavailable()); }
|
|
89
|
+
}
|
|
90
|
+
if (observations.length === 8) break;
|
|
91
|
+
}
|
|
92
|
+
return observations;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Diagnostic facts only: never browser authority, a receipt or replay permission. */
|
|
2
|
+
const phases = new Set(['queued', 'preflight', 'replay', 'target', 'actionability', 'navigate', 'dispatch', 'settle',
|
|
3
|
+
'capture_dom', 'capture_screenshot', 'capture_controls', 'publish', 'close']);
|
|
4
|
+
const outcomes = new Set(['ok', 'error', 'uncertain', 'timeout', 'canceled']);
|
|
5
|
+
const keys = ['contract', 'actionId', 'kind', 'phase', 'dispatchStarted', 'dispatchCompleted', 'elapsedMs', 'timeoutMs', 'outcome', 'errorCode'];
|
|
6
|
+
const safeCode = value => value === null || typeof value === 'string' && /^[a-z][a-z0-9_]{0,95}$/.test(value);
|
|
7
|
+
|
|
8
|
+
export function admitBrowserActionObservation(value, action) {
|
|
9
|
+
try {
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
11
|
+
|| Object.keys(value).length !== keys.length || !Object.keys(value).every(key => keys.includes(key))) return null;
|
|
12
|
+
value = Object.fromEntries(keys.map(key => [key, value[key]]));
|
|
13
|
+
if (value.contract !== 'browser-action-observation/v1' || value.actionId !== action.actionId || value.kind !== action.kind
|
|
14
|
+
|| !phases.has(value.phase) || !outcomes.has(value.outcome) || !safeCode(value.errorCode)
|
|
15
|
+
|| typeof value.dispatchStarted !== 'boolean' || typeof value.dispatchCompleted !== 'boolean'
|
|
16
|
+
|| value.dispatchCompleted && !value.dispatchStarted || value.timeoutMs !== action.timeoutMs
|
|
17
|
+
|| !Number.isSafeInteger(value.elapsedMs) || value.elapsedMs < 0) return null;
|
|
18
|
+
return value;
|
|
19
|
+
} catch { return null; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createBrowserActionAttempt(action) {
|
|
23
|
+
const startedAt = performance.now();
|
|
24
|
+
return { started: false, phase: 'queued', dispatchStarted: false, dispatchCompleted: false,
|
|
25
|
+
publish(observer, { signal, internal, observation, failureCode }) {
|
|
26
|
+
if (typeof observer !== 'function') return;
|
|
27
|
+
const errorCode = internal.aborted ? signal?.aborted ? 'action_canceled' : 'action_timed_out'
|
|
28
|
+
: observation?.errorCode ?? failureCode ?? null;
|
|
29
|
+
const value = admitBrowserActionObservation({ contract: 'browser-action-observation/v1',
|
|
30
|
+
actionId: action.actionId, kind: action.kind, phase: this.phase,
|
|
31
|
+
dispatchStarted: this.dispatchStarted, dispatchCompleted: this.dispatchCompleted,
|
|
32
|
+
elapsedMs: Math.max(0, Math.round(performance.now() - startedAt)), timeoutMs: action.timeoutMs,
|
|
33
|
+
outcome: internal.aborted ? signal?.aborted ? 'canceled' : 'timeout' : observation?.status ?? 'error',
|
|
34
|
+
errorCode: safeCode(errorCode) ? errorCode : 'browser_action_error',
|
|
35
|
+
}, action);
|
|
36
|
+
if (!value) return;
|
|
37
|
+
try {
|
|
38
|
+
const pending = observer(Object.freeze(value));
|
|
39
|
+
if (pending && typeof pending.then === 'function') void Promise.resolve(pending).catch(() => {});
|
|
40
|
+
} catch { /* Observer failure cannot change browser execution. */ }
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { accessSync, constants, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { chromium } from 'playwright-core';
|
|
4
|
+
|
|
5
|
+
function executable(path) {
|
|
6
|
+
try { return isAbsolute(path) && statSync(path).isFile() && (accessSync(path, constants.X_OK), true); }
|
|
7
|
+
catch { return false; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Use an installed executable with a fresh browser profile. Discovery performs
|
|
11
|
+
* no download, browser launch, user-profile access or permission grant. */
|
|
12
|
+
export function resolveNativeBrowserExecutable({ env = process.env, platform = process.platform,
|
|
13
|
+
bundledPath = chromium.executablePath(), canExecute = executable } = {}) {
|
|
14
|
+
const explicit = env.KSADK_BROWSER_EXECUTABLE_PATH;
|
|
15
|
+
if (explicit) return isAbsolute(explicit) && canExecute(explicit) ? explicit : null;
|
|
16
|
+
const candidates = [bundledPath,
|
|
17
|
+
...(platform === 'darwin' ? ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
18
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium'] : []),
|
|
19
|
+
...(platform === 'linux' ? ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome'] : []),
|
|
20
|
+
];
|
|
21
|
+
return candidates.find(path => typeof path === 'string' && isAbsolute(path) && canExecute(path)) ?? null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Host-side pixels, not page canvas APIs, supply diagnostic colour evidence.
|
|
2
|
+
* Python uses Pillow thumbnail/quantize. Node samples at most 64x64 pixels and
|
|
3
|
+
* uses a deterministic weighted median cut with at most 16 RGB buckets.
|
|
4
|
+
* Numeric values may differ; uniformity is diagnostic, never a pass verdict. */
|
|
5
|
+
import { PNG } from 'pngjs';
|
|
6
|
+
import { invariant } from '@kingsoftcloud/agentkit-harness-core';
|
|
7
|
+
|
|
8
|
+
// JSON persists -0 as 0. Emit canonical numbers before the first receipt so
|
|
9
|
+
// an in-memory result and its cold-restored durable receipt stay identical.
|
|
10
|
+
const round = n => Math.round(n * 10000) / 10000 || 0;
|
|
11
|
+
export function browserImageVariation(bytes) {
|
|
12
|
+
invariant(Buffer.isBuffer(bytes) && bytes.length >= 24 && bytes.length <= 16 * 1024 * 1024
|
|
13
|
+
&& bytes.readUInt32BE(16) * bytes.readUInt32BE(20) <= 16 * 1024 * 1024, 'browser_canvas_image_limit');
|
|
14
|
+
const image = PNG.sync.read(bytes), histogram = new Map();
|
|
15
|
+
const width = Math.min(64, image.width), height = Math.min(64, image.height);
|
|
16
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
|
|
17
|
+
const offset = (Math.floor((y + .5) * image.height / height) * image.width + Math.floor((x + .5) * image.width / width)) * 4;
|
|
18
|
+
const color = [...image.data.subarray(offset, offset + 3)], key = color.join(',');
|
|
19
|
+
const previous = histogram.get(key); if (previous) previous.count++; else histogram.set(key, { color, count: 1 });
|
|
20
|
+
}
|
|
21
|
+
const ranges = values => [0, 1, 2].map(channel => Math.max(...values.map(v => v.color[channel])) - Math.min(...values.map(v => v.color[channel])));
|
|
22
|
+
const buckets = [[...histogram.values()]];
|
|
23
|
+
while (buckets.length < 16) {
|
|
24
|
+
let selected = -1, range = 0, channel = 0;
|
|
25
|
+
buckets.forEach((values, i) => ranges(values).forEach((value, c) => { if (value > range) { selected = i; range = value; channel = c; } }));
|
|
26
|
+
if (selected === -1) break;
|
|
27
|
+
const values = buckets[selected].sort((a, b) => a.color[channel] - b.color[channel]);
|
|
28
|
+
const half = values.reduce((sum, value) => sum + value.count, 0) / 2;
|
|
29
|
+
let count = 0, index = 0;
|
|
30
|
+
do { count += values[index++].count; } while (count < half && index < values.length - 1);
|
|
31
|
+
buckets.splice(selected, 1, values.slice(0, index), values.slice(index));
|
|
32
|
+
}
|
|
33
|
+
const counts = buckets.map(values => values.reduce((sum, value) => sum + value.count, 0)), total = width * height;
|
|
34
|
+
const dominant = Math.max(...counts) / total;
|
|
35
|
+
return { colour_buckets: counts.length, dominant_ratio: round(dominant),
|
|
36
|
+
entropy_bits: round(-counts.reduce((sum, count) => { const p = count / total; return sum + p * Math.log2(p); }, 0)),
|
|
37
|
+
visually_uniform: counts.length <= 2 || dominant >= .995 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function browserCanvasMetrics(page, step) {
|
|
41
|
+
const result = [], canvases = page.locator('canvas'), count = Math.min(await step(() => canvases.count()), 3);
|
|
42
|
+
for (let index = 0; index < count; index++) {
|
|
43
|
+
const canvas = canvases.nth(index), box = await step(() => canvas.boundingBox());
|
|
44
|
+
if (!box || box.width < 8 || box.height < 8) continue;
|
|
45
|
+
const viewport = page.viewportSize() ?? { width: 1280, height: 720 };
|
|
46
|
+
let handle, hidden;
|
|
47
|
+
try {
|
|
48
|
+
handle = await step(() => canvas.elementHandle());
|
|
49
|
+
// The handle owns the exact nodes/styles. No page-controlled dataset or
|
|
50
|
+
// selector is used to recover the original visibility after capture.
|
|
51
|
+
hidden = await step(() => page.evaluateHandle(element => {
|
|
52
|
+
const saved = [];
|
|
53
|
+
for (const node of document.body.querySelectorAll('*')) {
|
|
54
|
+
if (node === element || node.contains(element)) continue;
|
|
55
|
+
saved.push([node, node.style.getPropertyValue('visibility'), node.style.getPropertyPriority('visibility')]);
|
|
56
|
+
node.style.setProperty('visibility', 'hidden', 'important');
|
|
57
|
+
}
|
|
58
|
+
return saved;
|
|
59
|
+
}, handle));
|
|
60
|
+
const bytes = await step(() => canvas.screenshot({ timeout: 5000 }));
|
|
61
|
+
result.push({ index, width: Math.round(box.width * 10) / 10, height: Math.round(box.height * 10) / 10,
|
|
62
|
+
viewport_coverage: round(box.width * box.height / (viewport.width * viewport.height)), ...browserImageVariation(bytes) });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
result.push({ index, error: String(error.code ?? error.name ?? 'canvas_capture_failed').slice(0, 500) });
|
|
65
|
+
} finally {
|
|
66
|
+
try { if (hidden) await step(() => hidden.evaluate(saved => { for (const [node, value, priority] of saved) node.style.setProperty('visibility', value, priority); })); }
|
|
67
|
+
finally { await hidden?.dispose().catch(() => {}); await handle?.dispose().catch(() => {}); }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/** Bounded host-owned DOM handles for the Native Playwright browser adapter.
|
|
2
|
+
*
|
|
3
|
+
* Ported from ksadk/harness/browser_adapters/native_dom_snapshot.py @ 355059d. One page task
|
|
4
|
+
* samples the DOM channels and returns metadata plus the exact elements that produced it.
|
|
5
|
+
* Input values stay excluded; page-provided text never mints reference IDs. Element handles are
|
|
6
|
+
* host-owned and stored only in this process-local map, never in page globals or DOM attributes.
|
|
7
|
+
*
|
|
8
|
+
* Format differences from the Python source (documented per the migration contract):
|
|
9
|
+
* - The page function returns camelCase meta/control keys (scrollX, scrollY, pointerLockActive,
|
|
10
|
+
* hitTestReachable, duplicateId) instead of Python's snake_case, matching the TS browser
|
|
11
|
+
* contract's camelCase BrowserObservation/BrowserControl. The capture logic is otherwise a
|
|
12
|
+
* faithful port of DOM_CAPTURE.
|
|
13
|
+
* - getProperties() returns a Map in Playwright JS (a dict in Python); iteration is adapted.
|
|
14
|
+
* - randomUUID() (with dashes stripped) replaces uuid4().hex; tokens remain 32 hex chars.
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
|
|
18
|
+
/** Self-contained page function. Runs in the page; must not close over any module variable.
|
|
19
|
+
* Returns { refElements, meta }. refElements is the live node list used to mint element refs;
|
|
20
|
+
* meta is the JSON-serializable observation sample (no input values). */
|
|
21
|
+
function domCapture() {
|
|
22
|
+
const semantic = Array.from(document.querySelectorAll(
|
|
23
|
+
'button,input,textarea,select,a[href],[role="button"],[contenteditable="true"]'
|
|
24
|
+
));
|
|
25
|
+
const pointer = Array.from(document.querySelectorAll('*')).slice(0, 4000).filter((el) => {
|
|
26
|
+
const isPointer = getComputedStyle(el).cursor === 'pointer';
|
|
27
|
+
return isPointer && (!el.parentElement || getComputedStyle(el.parentElement).cursor !== 'pointer');
|
|
28
|
+
});
|
|
29
|
+
const nodes = Array.from(new Set([...semantic, ...pointer]));
|
|
30
|
+
const visible = nodes.filter((el) => {
|
|
31
|
+
const rect = el.getBoundingClientRect();
|
|
32
|
+
const style = getComputedStyle(el);
|
|
33
|
+
const type = (el.getAttribute('type') || '').toLowerCase();
|
|
34
|
+
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden'
|
|
35
|
+
&& style.display !== 'none' && type !== 'password' && type !== 'hidden';
|
|
36
|
+
});
|
|
37
|
+
const structuralSelector = (el) => {
|
|
38
|
+
const parts = [];
|
|
39
|
+
let node = el;
|
|
40
|
+
while (node && node.nodeType === 1 && parts.length < 20) {
|
|
41
|
+
const id = node.id && globalThis.CSS && CSS.escape ? '#' + CSS.escape(node.id) : null;
|
|
42
|
+
if (id && document.querySelectorAll(id).length === 1) { parts.unshift(id); break; }
|
|
43
|
+
const tag = node.tagName.toLowerCase();
|
|
44
|
+
const siblings = node.parentElement
|
|
45
|
+
? Array.from(node.parentElement.children).filter((sibling) => sibling.tagName === node.tagName)
|
|
46
|
+
: [node];
|
|
47
|
+
parts.unshift(tag + ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')');
|
|
48
|
+
node = node.parentElement;
|
|
49
|
+
}
|
|
50
|
+
if (node && node.parentElement && parts.length >= 20) return null;
|
|
51
|
+
const candidate = parts.join(' > ');
|
|
52
|
+
return candidate && candidate.length <= 512 && document.querySelectorAll(candidate).length === 1
|
|
53
|
+
? candidate : null;
|
|
54
|
+
};
|
|
55
|
+
const refElements = visible.slice(0, 40);
|
|
56
|
+
const controls = refElements.map((el) => {
|
|
57
|
+
const tag = el.tagName.toLowerCase();
|
|
58
|
+
const rect = el.getBoundingClientRect();
|
|
59
|
+
const inViewport = rect.right > 0 && rect.bottom > 0 && rect.left < innerWidth && rect.top < innerHeight;
|
|
60
|
+
const hitX = Math.max(0, Math.min(innerWidth - 1,
|
|
61
|
+
(Math.max(0, rect.left) + Math.min(innerWidth, rect.right)) / 2));
|
|
62
|
+
const hitY = Math.max(0, Math.min(innerHeight - 1,
|
|
63
|
+
(Math.max(0, rect.top) + Math.min(innerHeight, rect.bottom)) / 2));
|
|
64
|
+
const hit = inViewport ? document.elementFromPoint(hitX, hitY) : null;
|
|
65
|
+
const id = el.id && globalThis.CSS && CSS.escape ? '#' + CSS.escape(el.id) : null;
|
|
66
|
+
const duplicateId = Boolean(id && document.querySelectorAll(id).length > 1);
|
|
67
|
+
const selector = duplicateId ? null : id || structuralSelector(el);
|
|
68
|
+
const label = el.getAttribute('aria-label') || (el.labels && el.labels[0] ? el.labels[0].innerText : '')
|
|
69
|
+
|| el.getAttribute('placeholder') || el.innerText || '';
|
|
70
|
+
return {
|
|
71
|
+
tag,
|
|
72
|
+
role: el.getAttribute('role') || '',
|
|
73
|
+
label: Array.from(String(label).trim()).slice(0, 160).join(''),
|
|
74
|
+
selector,
|
|
75
|
+
duplicateId,
|
|
76
|
+
clickable: tag === 'button' || el.getAttribute('role') === 'button'
|
|
77
|
+
|| getComputedStyle(el).cursor === 'pointer',
|
|
78
|
+
hitTestReachable: !inViewport ? null : !el.matches(':disabled') && (hit === el || el.contains(hit))
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
refElements,
|
|
83
|
+
meta: {
|
|
84
|
+
url: location.href,
|
|
85
|
+
title: document.title,
|
|
86
|
+
text: (document.body && document.body.innerText) || '',
|
|
87
|
+
focus: (document.activeElement && document.activeElement.tagName) || '',
|
|
88
|
+
scrollX: Math.round(window.scrollX),
|
|
89
|
+
scrollY: Math.round(window.scrollY),
|
|
90
|
+
lifecycle: document.readyState,
|
|
91
|
+
pointerLockActive: Boolean(document.pointerLockElement),
|
|
92
|
+
controls
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function disposeHandles(handles) {
|
|
98
|
+
for (const handle of handles) {
|
|
99
|
+
try { await handle.dispose(); } catch { /* navigation/closing may have released the context */ }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Latest bounded observation; unchanged DOM nodes retain their opaque ID across captures. */
|
|
104
|
+
export class BrowserElementRefs {
|
|
105
|
+
constructor() { this._elements = new Map(); }
|
|
106
|
+
|
|
107
|
+
async clear() {
|
|
108
|
+
const previous = this._elements;
|
|
109
|
+
this._elements = new Map();
|
|
110
|
+
await disposeHandles([...previous.values()]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Capture one DOM sample. Returns { meta, tokens } where tokens[i] is the opaque ref for
|
|
114
|
+
* control i (or null if no element was retained). Unchanged nodes keep their prior token. */
|
|
115
|
+
async capture(page) {
|
|
116
|
+
const transient = [];
|
|
117
|
+
const acquired = [];
|
|
118
|
+
let adopted = false;
|
|
119
|
+
try {
|
|
120
|
+
const collection = await page.evaluateHandle(domCapture);
|
|
121
|
+
transient.push(collection);
|
|
122
|
+
const metadata = await collection.getProperty('meta');
|
|
123
|
+
transient.push(metadata);
|
|
124
|
+
const nodes = await collection.getProperty('refElements');
|
|
125
|
+
transient.push(nodes);
|
|
126
|
+
const meta = await metadata.jsonValue();
|
|
127
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) throw new Error('invalid DOM snapshot');
|
|
128
|
+
|
|
129
|
+
const handles = new Map();
|
|
130
|
+
const props = await nodes.getProperties(); // Map<string, JSHandle>
|
|
131
|
+
for (const [index, handle] of props) {
|
|
132
|
+
acquired.push(handle);
|
|
133
|
+
const element = handle.asElement();
|
|
134
|
+
if (/^\d+$/.test(index) && Number(index) < 40 && element) handles.set(Number(index), element);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let priorTokens = [...this._elements.keys()];
|
|
138
|
+
let priorHandles = [...this._elements.values()];
|
|
139
|
+
if (priorHandles.length > 0) {
|
|
140
|
+
let sameDocument = false;
|
|
141
|
+
try { sameDocument = await priorHandles[0].evaluate((node) => node.ownerDocument === document); }
|
|
142
|
+
catch { sameDocument = false; }
|
|
143
|
+
if (!sameDocument) {
|
|
144
|
+
// A successful click can navigate without _navigate(); its old execution context cannot
|
|
145
|
+
// supply refs to the new document, which does not justify replaying that click.
|
|
146
|
+
await this.clear();
|
|
147
|
+
priorTokens = [];
|
|
148
|
+
priorHandles = [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Compare actual node identity in one browser task, without writing refs into an untrusted
|
|
153
|
+
// DOM/global map or one RPC per element pair.
|
|
154
|
+
const matches = handles.size > 0
|
|
155
|
+
? await nodes.evaluate((nodesArr, prior) => nodesArr.map((node) => prior.indexOf(node)), priorHandles)
|
|
156
|
+
: [];
|
|
157
|
+
|
|
158
|
+
const refs = new Map();
|
|
159
|
+
const tokens = [];
|
|
160
|
+
const controls = Array.isArray(meta.controls) ? meta.controls : [];
|
|
161
|
+
for (let index = 0; index < Math.min(40, controls.length); index++) {
|
|
162
|
+
const element = handles.get(index);
|
|
163
|
+
if (!element) { tokens.push(null); continue; }
|
|
164
|
+
const match = index < matches.length ? matches[index] : -1;
|
|
165
|
+
const token = (match >= 0 && match < priorTokens.length) ? priorTokens[match] : randomUUID().replace(/-/g, '');
|
|
166
|
+
refs.set(token, element);
|
|
167
|
+
tokens.push(token);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const previous = this._elements;
|
|
171
|
+
this._elements = refs;
|
|
172
|
+
adopted = true;
|
|
173
|
+
const retained = new Set(refs.values());
|
|
174
|
+
await disposeHandles([...previous.values(), ...acquired.filter((h) => !retained.has(h))]);
|
|
175
|
+
return { meta, tokens };
|
|
176
|
+
} finally {
|
|
177
|
+
if (!adopted) await disposeHandles(acquired);
|
|
178
|
+
await disposeHandles(transient);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Resolve a token to a live ElementHandle, or null if it is stale/detached/foreign. */
|
|
183
|
+
async resolve(token) {
|
|
184
|
+
const element = this._elements.get(token);
|
|
185
|
+
if (!element) return null;
|
|
186
|
+
try {
|
|
187
|
+
const connected = await element.evaluate((node) => node.isConnected);
|
|
188
|
+
return connected ? element : null;
|
|
189
|
+
} catch { return null; }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { domCapture, domCapture as DOM_CAPTURE };
|