@aiscene/shared 8.0.3 → 8.0.5
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/es/agent-tools/agent-behavior-init-args.mjs +44 -0
- package/dist/es/agent-tools/base-tools.mjs +163 -0
- package/dist/es/agent-tools/chrome-path.mjs +50 -0
- package/dist/es/agent-tools/cli-report-session.mjs +78 -0
- package/dist/es/agent-tools/error-formatter.mjs +106 -0
- package/dist/es/agent-tools/index.mjs +9 -0
- package/dist/es/agent-tools/init-arg-utils.mjs +38 -0
- package/dist/es/agent-tools/observation-artifact.mjs +5 -0
- package/dist/es/agent-tools/observation-record.mjs +248 -0
- package/dist/es/agent-tools/tool-defaults.mjs +54 -0
- package/dist/es/agent-tools/tool-generator.mjs +529 -0
- package/dist/es/agent-tools/types.mjs +3 -0
- package/dist/es/agent-tools/user-prompt.mjs +66 -0
- package/dist/es/cli/interrupt.mjs +116 -0
- package/dist/es/cli/record-command.mjs +130 -0
- package/dist/es/cli/screenshot-file.mjs +24 -0
- package/dist/es/cli/verbose-ai-act.mjs +230 -0
- package/dist/es/cli/verbose-screenshot.mjs +134 -0
- package/dist/es/cli/verbose.mjs +404 -0
- package/dist/es/env/parse-model-config.mjs +1 -1
- package/dist/es/env/types.mjs +18 -3
- package/dist/es/img/index.mjs +3 -3
- package/dist/es/img/info.mjs +45 -1
- package/dist/es/img/photon-loader.mjs +4 -0
- package/dist/es/img/transform.mjs +113 -3
- package/dist/es/recorder.mjs +249 -0
- package/dist/lib/agent-tools/agent-behavior-init-args.js +87 -0
- package/dist/lib/agent-tools/base-tools.js +197 -0
- package/dist/lib/agent-tools/chrome-path.js +87 -0
- package/dist/lib/agent-tools/cli-report-session.js +121 -0
- package/dist/lib/agent-tools/error-formatter.js +149 -0
- package/dist/lib/agent-tools/index.js +114 -0
- package/dist/lib/agent-tools/init-arg-utils.js +78 -0
- package/dist/lib/agent-tools/observation-artifact.js +42 -0
- package/dist/lib/agent-tools/observation-record.js +297 -0
- package/dist/lib/agent-tools/tool-defaults.js +97 -0
- package/dist/lib/agent-tools/tool-generator.js +569 -0
- package/dist/lib/agent-tools/types.js +40 -0
- package/dist/lib/agent-tools/user-prompt.js +103 -0
- package/dist/lib/cli/interrupt.js +156 -0
- package/dist/lib/cli/record-command.js +164 -0
- package/dist/lib/cli/screenshot-file.js +58 -0
- package/dist/lib/cli/verbose-ai-act.js +273 -0
- package/dist/lib/cli/verbose-screenshot.js +177 -0
- package/dist/lib/cli/verbose.js +465 -0
- package/dist/lib/env/parse-model-config.js +1 -1
- package/dist/lib/env/types.js +32 -2
- package/dist/lib/img/index.js +29 -5
- package/dist/lib/img/info.js +48 -1
- package/dist/lib/img/photon-loader.js +38 -0
- package/dist/lib/img/transform.js +135 -4
- package/dist/lib/recorder.js +307 -0
- package/dist/types/agent-tools/agent-behavior-init-args.d.ts +17 -0
- package/dist/types/agent-tools/base-tools.d.ts +158 -0
- package/dist/types/agent-tools/chrome-path.d.ts +2 -0
- package/dist/types/agent-tools/cli-report-session.d.ts +12 -0
- package/dist/types/agent-tools/error-formatter.d.ts +30 -0
- package/dist/types/agent-tools/index.d.ts +9 -0
- package/dist/types/agent-tools/init-arg-utils.d.ts +13 -0
- package/dist/types/agent-tools/observation-artifact.d.ts +10 -0
- package/dist/types/agent-tools/observation-record.d.ts +38 -0
- package/dist/types/agent-tools/tool-defaults.d.ts +63 -0
- package/dist/types/agent-tools/tool-generator.d.ts +13 -0
- package/dist/types/agent-tools/types.d.ts +213 -0
- package/dist/types/agent-tools/user-prompt.d.ts +13 -0
- package/dist/types/cli/interrupt.d.ts +49 -0
- package/dist/types/cli/record-command.d.ts +3 -0
- package/dist/types/cli/screenshot-file.d.ts +10 -0
- package/dist/types/cli/verbose-ai-act.d.ts +44 -0
- package/dist/types/cli/verbose-screenshot.d.ts +10 -0
- package/dist/types/cli/verbose.d.ts +40 -0
- package/dist/types/env/types.d.ts +16 -7
- package/dist/types/img/index.d.ts +2 -2
- package/dist/types/img/info.d.ts +2 -0
- package/dist/types/img/photon-loader.d.ts +2 -0
- package/dist/types/img/transform.d.ts +22 -2
- package/dist/types/mcp/types.d.ts +1 -0
- package/dist/types/recorder.d.ts +113 -0
- package/package.json +1 -1
- package/src/agent-tools/agent-behavior-init-args.ts +109 -0
- package/src/agent-tools/base-tools.ts +399 -0
- package/src/agent-tools/chrome-path.ts +74 -0
- package/src/agent-tools/cli-report-session.ts +130 -0
- package/src/agent-tools/error-formatter.ts +177 -0
- package/src/agent-tools/index.ts +9 -0
- package/src/agent-tools/init-arg-utils.ts +105 -0
- package/src/agent-tools/observation-artifact.ts +29 -0
- package/src/agent-tools/observation-record.ts +331 -0
- package/src/agent-tools/tool-defaults.ts +119 -0
- package/src/agent-tools/tool-generator.ts +866 -0
- package/src/agent-tools/types.ts +250 -0
- package/src/agent-tools/user-prompt.ts +102 -0
- package/src/cli/interrupt.ts +207 -0
- package/src/cli/record-command.ts +177 -0
- package/src/cli/screenshot-file.ts +61 -0
- package/src/cli/verbose-ai-act.ts +387 -0
- package/src/cli/verbose-screenshot.ts +269 -0
- package/src/cli/verbose.ts +753 -0
- package/src/env/types.ts +30 -2
- package/src/img/index.ts +12 -0
- package/src/img/info.ts +61 -0
- package/src/img/photon-loader.ts +5 -0
- package/src/img/transform.ts +262 -3
- package/src/mcp/types.ts +2 -0
- package/src/recorder.ts +625 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getErrorMessage } from '../agent-tools/error-formatter';
|
|
3
|
+
import { resolveObservationArtifactAdapter } from '../agent-tools/observation-artifact';
|
|
4
|
+
import { writeUIObservationRecord } from '../agent-tools/observation-record';
|
|
5
|
+
import type {
|
|
6
|
+
BaseAgent,
|
|
7
|
+
ToolCliMetadata,
|
|
8
|
+
ToolDefinition,
|
|
9
|
+
ToolResult,
|
|
10
|
+
ToolSchema,
|
|
11
|
+
} from '../agent-tools/types';
|
|
12
|
+
import { type CliInterruptWaiter, createCliInterruptWaiter } from './interrupt';
|
|
13
|
+
import { attachCliVerboseDumpListener, emitCliVerboseEvent } from './verbose';
|
|
14
|
+
|
|
15
|
+
const recordCliMetadata: ToolCliMetadata = {
|
|
16
|
+
positionals: ['action'],
|
|
17
|
+
options: {
|
|
18
|
+
intervalMs: {
|
|
19
|
+
preferredName: 'interval-ms',
|
|
20
|
+
aliases: ['intervalMs'],
|
|
21
|
+
},
|
|
22
|
+
maxFrames: {
|
|
23
|
+
preferredName: 'max-frames',
|
|
24
|
+
aliases: ['maxFrames'],
|
|
25
|
+
},
|
|
26
|
+
watchdogMs: {
|
|
27
|
+
preferredName: 'watchdog-ms',
|
|
28
|
+
aliases: ['watchdogMs'],
|
|
29
|
+
},
|
|
30
|
+
action: {
|
|
31
|
+
hidden: true,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function mergeCliMetadata(
|
|
37
|
+
base: ToolCliMetadata,
|
|
38
|
+
extra?: ToolCliMetadata,
|
|
39
|
+
): ToolCliMetadata {
|
|
40
|
+
return {
|
|
41
|
+
positionals: base.positionals,
|
|
42
|
+
options: {
|
|
43
|
+
...(base.options ?? {}),
|
|
44
|
+
...(extra?.options ?? {}),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createErrorResult(message: string): ToolResult {
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: 'text', text: message }],
|
|
52
|
+
isError: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Create the foreground recording command exposed only by Midscene CLIs. */
|
|
57
|
+
export function createRecordCliCommand(
|
|
58
|
+
getAgent: (args?: Record<string, unknown>) => Promise<BaseAgent>,
|
|
59
|
+
initArgSchema: ToolSchema = {},
|
|
60
|
+
initArgCliMetadata?: ToolCliMetadata,
|
|
61
|
+
): ToolDefinition {
|
|
62
|
+
return {
|
|
63
|
+
name: 'record',
|
|
64
|
+
description:
|
|
65
|
+
'Record the page/screen in the foreground until Ctrl+C or a termination signal, then save the ordered frame window for a later assert command.',
|
|
66
|
+
schema: {
|
|
67
|
+
action: z
|
|
68
|
+
.literal('start')
|
|
69
|
+
.describe('Start a foreground recording. Press Ctrl+C to finish.'),
|
|
70
|
+
output: z
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe(
|
|
74
|
+
'Path for the JSON observation manifest. Frames are stored in an adjacent <name>.frames directory. Defaults to a generated path under midscene_run/output.',
|
|
75
|
+
),
|
|
76
|
+
intervalMs: z
|
|
77
|
+
.number()
|
|
78
|
+
.min(200)
|
|
79
|
+
.optional()
|
|
80
|
+
.describe(
|
|
81
|
+
'Sampling interval in milliseconds. Defaults to 1000; minimum 200 (5fps).',
|
|
82
|
+
),
|
|
83
|
+
maxFrames: z
|
|
84
|
+
.number()
|
|
85
|
+
.int()
|
|
86
|
+
.min(2)
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Maximum buffered frames. Defaults to 30; minimum 2.'),
|
|
89
|
+
watchdogMs: z
|
|
90
|
+
.number()
|
|
91
|
+
.min(0)
|
|
92
|
+
.optional()
|
|
93
|
+
.describe(
|
|
94
|
+
'Safety timeout in milliseconds. Defaults to 300000 (5 minutes); 0 disables it.',
|
|
95
|
+
),
|
|
96
|
+
...initArgSchema,
|
|
97
|
+
},
|
|
98
|
+
cli: mergeCliMetadata(recordCliMetadata, initArgCliMetadata),
|
|
99
|
+
handler: async (
|
|
100
|
+
args: Record<string, unknown> = {},
|
|
101
|
+
): Promise<ToolResult> => {
|
|
102
|
+
if (args.action !== 'start') {
|
|
103
|
+
return createErrorResult(
|
|
104
|
+
'record requires the start operation (for example: record start --output ./observation.json)',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const agent = await getAgent(args);
|
|
110
|
+
emitCliVerboseEvent({ event: 'agent_ready', tool: 'record' });
|
|
111
|
+
if (!agent.startObserving) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
'record is not supported because this agent does not provide startObserving',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const observationArtifacts = resolveObservationArtifactAdapter(agent);
|
|
117
|
+
if (!observationArtifacts) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
'record is not supported because this agent does not provide observation artifact persistence',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const unsubscribeVerbose = attachCliVerboseDumpListener(agent, {
|
|
123
|
+
toolName: 'record',
|
|
124
|
+
});
|
|
125
|
+
let observer:
|
|
126
|
+
| Awaited<ReturnType<NonNullable<BaseAgent['startObserving']>>>
|
|
127
|
+
| undefined;
|
|
128
|
+
let interruptWaiter: CliInterruptWaiter | undefined;
|
|
129
|
+
try {
|
|
130
|
+
const watchdogMs = (args.watchdogMs as number | undefined) ?? 300_000;
|
|
131
|
+
observer = await agent.startObserving({
|
|
132
|
+
intervalMs: args.intervalMs as number | undefined,
|
|
133
|
+
maxFrames: args.maxFrames as number | undefined,
|
|
134
|
+
watchdogMs,
|
|
135
|
+
});
|
|
136
|
+
interruptWaiter = createCliInterruptWaiter(watchdogMs);
|
|
137
|
+
emitCliVerboseEvent({
|
|
138
|
+
event: 'recording_ready',
|
|
139
|
+
tool: 'record',
|
|
140
|
+
watchdogMs,
|
|
141
|
+
});
|
|
142
|
+
const stopReason = await interruptWaiter.result;
|
|
143
|
+
emitCliVerboseEvent({
|
|
144
|
+
event: 'recording_stopping',
|
|
145
|
+
tool: 'record',
|
|
146
|
+
reason: stopReason,
|
|
147
|
+
});
|
|
148
|
+
const observation = await observer.stop();
|
|
149
|
+
const record = await observationArtifacts.exportRecord(observation);
|
|
150
|
+
const outputPath = writeUIObservationRecord(
|
|
151
|
+
record,
|
|
152
|
+
args.output as string | undefined,
|
|
153
|
+
);
|
|
154
|
+
return {
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: 'text',
|
|
158
|
+
text: `Observation record saved: ${outputPath}`,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
} finally {
|
|
163
|
+
try {
|
|
164
|
+
await observer?.dispose?.();
|
|
165
|
+
} finally {
|
|
166
|
+
interruptWaiter?.dispose();
|
|
167
|
+
unsubscribeVerbose();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (error: unknown) {
|
|
171
|
+
const errorMessage = getErrorMessage(error);
|
|
172
|
+
console.error('Error executing record:', errorMessage);
|
|
173
|
+
return createErrorResult(`Failed to execute record: ${errorMessage}`);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export interface WriteCliScreenshotFileOptions {
|
|
6
|
+
id?: unknown;
|
|
7
|
+
mimeType?: unknown;
|
|
8
|
+
extension?: unknown;
|
|
9
|
+
directoryPath?: string;
|
|
10
|
+
directoryName?: string;
|
|
11
|
+
filenamePrefix?: string;
|
|
12
|
+
overwrite?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function safeScreenshotFilenamePart(value: unknown): string {
|
|
16
|
+
const text = typeof value === 'string' && value.length > 0 ? value : 'shot';
|
|
17
|
+
return text.replace(/[^a-zA-Z0-9._-]/g, '_') || 'shot';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function extensionFromImageMetadata(
|
|
21
|
+
mimeType: unknown,
|
|
22
|
+
extension: unknown,
|
|
23
|
+
): 'png' | 'jpeg' {
|
|
24
|
+
if (extension === 'jpeg' || extension === 'jpg') {
|
|
25
|
+
return 'jpeg';
|
|
26
|
+
}
|
|
27
|
+
if (extension === 'png') {
|
|
28
|
+
return 'png';
|
|
29
|
+
}
|
|
30
|
+
return mimeType === 'image/jpeg' ? 'jpeg' : 'png';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function writeCliScreenshotFile(
|
|
34
|
+
rawBase64: string,
|
|
35
|
+
options: WriteCliScreenshotFileOptions = {},
|
|
36
|
+
): string {
|
|
37
|
+
const extension = extensionFromImageMetadata(
|
|
38
|
+
options.mimeType,
|
|
39
|
+
options.extension,
|
|
40
|
+
);
|
|
41
|
+
const directory = options.directoryPath
|
|
42
|
+
? options.directoryPath
|
|
43
|
+
: options.directoryName
|
|
44
|
+
? join(tmpdir(), options.directoryName)
|
|
45
|
+
: tmpdir();
|
|
46
|
+
if (!existsSync(directory)) {
|
|
47
|
+
mkdirSync(directory, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const filename =
|
|
51
|
+
options.id !== undefined
|
|
52
|
+
? `${safeScreenshotFilenamePart(options.id)}.${extension}`
|
|
53
|
+
: `${options.filenamePrefix ?? 'screenshot'}-${Date.now()}.${extension}`;
|
|
54
|
+
const filePath = join(directory, filename);
|
|
55
|
+
|
|
56
|
+
if (options.overwrite !== false || !existsSync(filePath)) {
|
|
57
|
+
writeFileSync(filePath, Buffer.from(rawBase64, 'base64'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return filePath;
|
|
61
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CliVerboseScreenshotCollectOptions,
|
|
3
|
+
collectScreenshotRefs,
|
|
4
|
+
pathForReportScreenshot,
|
|
5
|
+
} from './verbose-screenshot';
|
|
6
|
+
|
|
7
|
+
export interface CliVerboseLine {
|
|
8
|
+
key: string;
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Generic progress-bus envelope as seen by the CLI (all fields untrusted). */
|
|
13
|
+
export interface CliAgentProgressEvent {
|
|
14
|
+
scope?: unknown;
|
|
15
|
+
phase?: unknown;
|
|
16
|
+
sequence?: unknown;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Flattened, render-ready aiAct payload derived from the envelope. */
|
|
21
|
+
export interface CliAiActProgressPayload {
|
|
22
|
+
phase?: unknown;
|
|
23
|
+
sequence?: unknown;
|
|
24
|
+
prompt?: unknown;
|
|
25
|
+
planIndex?: unknown;
|
|
26
|
+
planLimit?: unknown;
|
|
27
|
+
action?: unknown;
|
|
28
|
+
thought?: unknown;
|
|
29
|
+
log?: unknown;
|
|
30
|
+
output?: unknown;
|
|
31
|
+
durationMs?: unknown;
|
|
32
|
+
error?: unknown;
|
|
33
|
+
screenshots?: Array<Record<string, unknown>>;
|
|
34
|
+
screenshotPath?: string;
|
|
35
|
+
// Lets the payload satisfy the generic `Record<string, unknown>` dispatch
|
|
36
|
+
// boundary while keeping the named fields documented.
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** aiAct scope tag on the progress bus (mirrors core's `aiActProgressScope`). */
|
|
41
|
+
export const cliAiActProgressScope = 'aiAct';
|
|
42
|
+
|
|
43
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compactText(value: unknown): string {
|
|
48
|
+
if (value === undefined || value === null || value === '') {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
return value.length > 180 ? `${value.slice(0, 177)}...` : value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
57
|
+
return String(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const json = JSON.stringify(value);
|
|
62
|
+
return json.length > 180 ? `${json.slice(0, 177)}...` : json;
|
|
63
|
+
} catch {
|
|
64
|
+
return String(value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function numericValue(value: unknown): number | undefined {
|
|
69
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
70
|
+
? value
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function firstText(values: unknown[]): string {
|
|
75
|
+
for (const value of values) {
|
|
76
|
+
const text = compactText(value);
|
|
77
|
+
if (text) {
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function planPrefix(event: CliAiActProgressPayload): string {
|
|
85
|
+
const planIndex = numericValue(event.planIndex);
|
|
86
|
+
if (!planIndex) {
|
|
87
|
+
return '[Midscene][aiAct]';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const planLimit = numericValue(event.planLimit);
|
|
91
|
+
return `[Midscene][aiAct][Plan ${Math.round(planIndex)}${
|
|
92
|
+
planLimit ? `/${Math.round(planLimit)}` : ''
|
|
93
|
+
}]`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function eventKey(event: CliAiActProgressPayload, suffix: string): string {
|
|
97
|
+
const sequence = numericValue(event.sequence);
|
|
98
|
+
if (sequence) {
|
|
99
|
+
return `aiAct:${sequence}:${suffix}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return [
|
|
103
|
+
'aiAct',
|
|
104
|
+
compactText(event.phase),
|
|
105
|
+
compactText(event.planIndex),
|
|
106
|
+
suffix,
|
|
107
|
+
]
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(':');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Structured action -> human-readable text (presentation lives here) ---
|
|
113
|
+
|
|
114
|
+
interface ParsedAiActAction {
|
|
115
|
+
name: string;
|
|
116
|
+
target?: string;
|
|
117
|
+
point?: [number, number];
|
|
118
|
+
bbox?: [number, number, number, number];
|
|
119
|
+
param?: unknown;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function integerText(value: number): string {
|
|
123
|
+
return String(Math.round(value));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function numberTuple2(value: unknown): [number, number] | undefined {
|
|
127
|
+
if (
|
|
128
|
+
Array.isArray(value) &&
|
|
129
|
+
value.length >= 2 &&
|
|
130
|
+
typeof value[0] === 'number' &&
|
|
131
|
+
typeof value[1] === 'number'
|
|
132
|
+
) {
|
|
133
|
+
return [value[0], value[1]];
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function numberTuple4(
|
|
139
|
+
value: unknown,
|
|
140
|
+
): [number, number, number, number] | undefined {
|
|
141
|
+
if (
|
|
142
|
+
Array.isArray(value) &&
|
|
143
|
+
value.length >= 4 &&
|
|
144
|
+
value.slice(0, 4).every((item) => typeof item === 'number')
|
|
145
|
+
) {
|
|
146
|
+
return [value[0], value[1], value[2], value[3]];
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseAction(value: unknown): ParsedAiActAction | undefined {
|
|
152
|
+
if (!isRecord(value) || typeof value.name !== 'string') {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
name: value.name,
|
|
157
|
+
target: typeof value.target === 'string' ? value.target : undefined,
|
|
158
|
+
point: numberTuple2(value.point),
|
|
159
|
+
bbox: numberTuple4(value.bbox),
|
|
160
|
+
param: value.param,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatPoint(point: [number, number]): string {
|
|
165
|
+
return `(${integerText(point[0])}, ${integerText(point[1])})`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function formatBbox(bbox: [number, number, number, number]): string {
|
|
169
|
+
return `(${bbox.map(integerText).join(',')})`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sleepActionText(action: ParsedAiActAction): string | undefined {
|
|
173
|
+
if (action.name !== 'Sleep' || !isRecord(action.param)) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const timeMs =
|
|
177
|
+
numericValue(action.param.timeMs) ??
|
|
178
|
+
numericValue(action.param.duration) ??
|
|
179
|
+
numericValue(action.param.timeoutMs);
|
|
180
|
+
return timeMs !== undefined ? `Sleep ${integerText(timeMs)}ms` : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function plannedActionText(action: ParsedAiActAction): string {
|
|
184
|
+
if (action.point) {
|
|
185
|
+
const targetSegment = action.target ? ` "${action.target}"` : '';
|
|
186
|
+
const bboxSegment = action.bbox ? `, bbox=${formatBbox(action.bbox)}` : '';
|
|
187
|
+
return `${action.name}${targetSegment} at ${formatPoint(action.point)}${bboxSegment}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const sleep = sleepActionText(action);
|
|
191
|
+
if (sleep) {
|
|
192
|
+
return sleep;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const paramText = compactText(action.param);
|
|
196
|
+
return paramText ? `${action.name}: ${paramText}` : action.name;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function runningActionText(action: ParsedAiActAction): string {
|
|
200
|
+
if (action.point) {
|
|
201
|
+
return `${action.name} at ${formatPoint(action.point)}`;
|
|
202
|
+
}
|
|
203
|
+
return plannedActionText(action);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function normalizeAiActProgressEventForCli(
|
|
207
|
+
event: unknown,
|
|
208
|
+
screenshotOptions: CliVerboseScreenshotCollectOptions = {},
|
|
209
|
+
): CliAiActProgressPayload | undefined {
|
|
210
|
+
if (!isRecord(event) || event.scope !== cliAiActProgressScope) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
215
|
+
|
|
216
|
+
const screenshots = collectScreenshotRefs(data.screenshot, screenshotOptions);
|
|
217
|
+
const latestPath = screenshots
|
|
218
|
+
.slice()
|
|
219
|
+
.reverse()
|
|
220
|
+
.find(
|
|
221
|
+
(item) => typeof item.path === 'string' && item.path.length > 0,
|
|
222
|
+
)?.path;
|
|
223
|
+
const screenshotPath =
|
|
224
|
+
typeof latestPath === 'string'
|
|
225
|
+
? pathForReportScreenshot(latestPath, screenshotOptions.reportFile)
|
|
226
|
+
: undefined;
|
|
227
|
+
|
|
228
|
+
const {
|
|
229
|
+
prompt,
|
|
230
|
+
planIndex,
|
|
231
|
+
planLimit,
|
|
232
|
+
action,
|
|
233
|
+
thought,
|
|
234
|
+
log,
|
|
235
|
+
output,
|
|
236
|
+
durationMs,
|
|
237
|
+
error,
|
|
238
|
+
} = data;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
phase: event.phase,
|
|
242
|
+
sequence: event.sequence,
|
|
243
|
+
prompt,
|
|
244
|
+
planIndex,
|
|
245
|
+
planLimit,
|
|
246
|
+
action,
|
|
247
|
+
thought,
|
|
248
|
+
log,
|
|
249
|
+
output,
|
|
250
|
+
durationMs,
|
|
251
|
+
error,
|
|
252
|
+
...(screenshots.length > 0 ? { screenshots } : {}),
|
|
253
|
+
...(screenshotPath ? { screenshotPath } : {}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function buildAiActProgressEventLines(
|
|
258
|
+
event: CliAiActProgressPayload,
|
|
259
|
+
): CliVerboseLine[] {
|
|
260
|
+
const phase = typeof event.phase === 'string' ? event.phase : '';
|
|
261
|
+
const prefix = planPrefix(event);
|
|
262
|
+
const error = compactText(event.error);
|
|
263
|
+
const duration =
|
|
264
|
+
typeof event.durationMs === 'number'
|
|
265
|
+
? ` cost=${Math.round(event.durationMs)}ms`
|
|
266
|
+
: '';
|
|
267
|
+
const action = parseAction(event.action);
|
|
268
|
+
|
|
269
|
+
switch (phase) {
|
|
270
|
+
case 'start': {
|
|
271
|
+
const prompt = compactText(event.prompt);
|
|
272
|
+
return [
|
|
273
|
+
{
|
|
274
|
+
key: `aiAct:start:${prompt}`,
|
|
275
|
+
text: prompt
|
|
276
|
+
? `[Midscene][aiAct] Start: ${prompt}`
|
|
277
|
+
: '[Midscene][aiAct] Start',
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
case 'plan_thinking':
|
|
282
|
+
return event.screenshotPath
|
|
283
|
+
? [
|
|
284
|
+
{
|
|
285
|
+
key: eventKey(event, 'thinking'),
|
|
286
|
+
text: `${prefix} Thinking with the latest screenshot: ${event.screenshotPath}`,
|
|
287
|
+
},
|
|
288
|
+
]
|
|
289
|
+
: [
|
|
290
|
+
{
|
|
291
|
+
key: eventKey(event, 'thinking'),
|
|
292
|
+
text: `${prefix} Thinking with the latest screenshot`,
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
case 'plan_planned': {
|
|
296
|
+
const text = firstText([event.log, event.thought, event.output]);
|
|
297
|
+
return text
|
|
298
|
+
? [
|
|
299
|
+
{
|
|
300
|
+
key: eventKey(event, 'planned'),
|
|
301
|
+
text: `${prefix} Planned: ${text}`,
|
|
302
|
+
},
|
|
303
|
+
]
|
|
304
|
+
: [];
|
|
305
|
+
}
|
|
306
|
+
case 'plan_action':
|
|
307
|
+
return action
|
|
308
|
+
? [
|
|
309
|
+
{
|
|
310
|
+
key: eventKey(event, 'action'),
|
|
311
|
+
text: `${prefix} Action: ${plannedActionText(action)}`,
|
|
312
|
+
},
|
|
313
|
+
]
|
|
314
|
+
: [];
|
|
315
|
+
case 'plan_failed':
|
|
316
|
+
return [
|
|
317
|
+
{
|
|
318
|
+
key: eventKey(event, 'plan_failed'),
|
|
319
|
+
text: `${prefix} Failed${error ? `: ${error}` : ''}`,
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
case 'action_running':
|
|
323
|
+
return action
|
|
324
|
+
? [
|
|
325
|
+
{
|
|
326
|
+
key: eventKey(event, 'action_running'),
|
|
327
|
+
text: `[Midscene][aiAct][Action] Running: ${runningActionText(action)}`,
|
|
328
|
+
},
|
|
329
|
+
]
|
|
330
|
+
: [];
|
|
331
|
+
case 'action_done':
|
|
332
|
+
return action
|
|
333
|
+
? [
|
|
334
|
+
{
|
|
335
|
+
key: eventKey(event, 'action_done'),
|
|
336
|
+
text: `[Midscene][aiAct][Action] Done: ${action.name}${duration}`,
|
|
337
|
+
},
|
|
338
|
+
]
|
|
339
|
+
: [];
|
|
340
|
+
case 'action_failed':
|
|
341
|
+
return [
|
|
342
|
+
{
|
|
343
|
+
key: eventKey(event, 'action_failed'),
|
|
344
|
+
text: `[Midscene][aiAct][Action] Failed${
|
|
345
|
+
action ? `: ${action.name}` : ''
|
|
346
|
+
}${duration}${error ? ` error=${error}` : ''}`,
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
case 'complete': {
|
|
350
|
+
const text = firstText([event.output, event.log, event.thought]);
|
|
351
|
+
return [
|
|
352
|
+
{
|
|
353
|
+
key: eventKey(event, 'complete'),
|
|
354
|
+
text: `[Midscene][aiAct] Complete${text ? `: ${text}` : ''}`,
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
case 'failed':
|
|
359
|
+
return [
|
|
360
|
+
{
|
|
361
|
+
key: eventKey(event, 'failed'),
|
|
362
|
+
text: `[Midscene][aiAct] Failed${error ? `: ${error}` : ''}`,
|
|
363
|
+
},
|
|
364
|
+
];
|
|
365
|
+
default:
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* A producer-specific renderer for the generic CLI progress dispatcher: turn a
|
|
372
|
+
* raw progress envelope into a normalized payload, then that payload into
|
|
373
|
+
* verbose lines. New producers register their own renderer; the dispatcher core
|
|
374
|
+
* stays untouched.
|
|
375
|
+
*/
|
|
376
|
+
export interface CliProgressRenderer {
|
|
377
|
+
normalize(
|
|
378
|
+
event: unknown,
|
|
379
|
+
screenshotOptions: CliVerboseScreenshotCollectOptions,
|
|
380
|
+
): Record<string, unknown> | undefined;
|
|
381
|
+
buildLines(payload: Record<string, unknown>): CliVerboseLine[];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export const aiActCliProgressRenderer: CliProgressRenderer = {
|
|
385
|
+
normalize: normalizeAiActProgressEventForCli,
|
|
386
|
+
buildLines: buildAiActProgressEventLines,
|
|
387
|
+
};
|