Skip to content

Commit 772272a

Browse files
authored
Disable runtime YAML result interpolation; apply env-var interpolation before execution and update docs/tests (#2577)
* fix(core): remove yaml runtime result interpolation * docs(site): simplify yaml result name docs
1 parent 5fce382 commit 772272a

9 files changed

Lines changed: 83 additions & 114 deletions

File tree

apps/site/docs/en/automate-with-scripts-in-yaml.mdx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -544,26 +544,21 @@ tasks:
544544
# ...
545545
```
546546

547-
#### Reuse Step Results
547+
#### Step Result Names
548548

549-
Steps that write a result with `name` can be referenced by later steps in the same YAML run. Use `$name` when the whole field should be replaced by the stored value.
549+
Steps that write a result with `name` save that value into the YAML run result and the JSON output. Use `name` to label values that should appear in the run output.
550550

551551
```yaml
552552
tasks:
553-
- name: Search by extracted data
553+
- name: Save extracted data
554554
flow:
555555
- aiString: Get the product id shown on the page
556556
name: product_id
557557

558-
- aiInput: Search box
559-
value: $product_id
560-
561558
- aiQuery: Get the search result after submitting the product id
562559
name: search_result
563560
```
564561
565-
`$name` keeps the original result type when the referenced value is used as the whole field. Runtime interpolation with `${name}` is also supported inside longer strings; non-string values are serialized as JSON before interpolation. Referencing an undefined variable throws an error.
566-
567562
#### Upload Files With `aiTap`
568563

569564
When clicking a button opens a file chooser, you can set `fileChooserAccept` directly on the `aiTap` step. It accepts either a single path or an array of paths.

apps/site/docs/en/changelog.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# Changelog
22

3-
## v1.8 - YAML Variables, Device Integrations & Model Behavior Updates
3+
## v1.8 - YAML Workflow, Device Integrations & Model Behavior Updates
44

5-
v1.8 adds YAML result reuse, refines model reasoning defaults, and brings new device/platform integration options.
5+
v1.8 refines model reasoning defaults and brings new device/platform integration options.
66

77
### YAML Workflow Enhancements
88

9-
- YAML steps with `name` can be reused in later steps through `$name` and `${name}`. See: [Automate with Scripts in YAML](./automate-with-scripts-in-yaml#reuse-step-results)
109
- Android `runAdbShell` now accepts a `timeout` option in both the JavaScript API and YAML scripts. See: [Android API](./android-api-reference), [Automate with Scripts in YAML](./automate-with-scripts-in-yaml)
1110

1211
### Device and Platform Integrations

apps/site/docs/en/yaml-script-runner.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ The CLI prints execution progress and generates a visual report when it finishes
124124

125125
### Use environment variables in `.yaml`
126126

127-
Reference environment variables in your scripts with `${variable-name}`.
127+
Reference environment variables in your scripts with `${variable-name}`. Environment-variable interpolation is applied before YAML task execution, including task strings.
128128

129129
```ini filename=.env
130130
topic=weather today

apps/site/docs/zh/automate-with-scripts-in-yaml.mdx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -551,26 +551,21 @@ tasks:
551551
# ...
552552
```
553553

554-
#### 复用步骤结果
554+
#### 步骤结果名称
555555

556-
带有 `name` 的步骤会把结果写入当前 YAML 运行的结果中,后续步骤可以引用这些结果。整个字段都需要替换成结果值时,使用 `$name`
556+
带有 `name` 的步骤会把结果写入当前 YAML 运行结果和 JSON 输出中。使用 `name` 标记需要出现在运行结果里的值
557557

558558
```yaml
559559
tasks:
560-
- name: 使用提取结果搜索
560+
- name: 保存提取结果
561561
flow:
562562
- aiString: 读取页面中的商品 id
563563
name: product_id
564564

565-
- aiInput: 搜索框
566-
value: $product_id
567-
568565
- aiQuery: 提交商品 id 后,获取搜索结果
569566
name: search_result
570567
```
571568
572-
`$name` 作为整个字段使用时,会保留原始结果类型。运行时也支持在更长的字符串中使用 `${name}` 插值;如果结果不是字符串,会先序列化为 JSON 再插入。引用未定义的变量会抛出错误。
573-
574569
#### 使用 `aiTap` 上传文件
575570

576571
当点击某个按钮会弹出文件选择器时,可以在 `aiTap` 步骤上直接设置 `fileChooserAccept`。它支持单个路径,也支持路径数组。

apps/site/docs/zh/changelog.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# 更新日志
22

3-
## v1.8 - YAML 变量复用、设备集成与模型行为更新
3+
## v1.8 - YAML 工作流、设备集成与模型行为更新
44

5-
v1.8 版本新增 YAML 步骤结果复用能力,调整模型思考模式默认行为,并补充了多项设备与平台集成能力。
5+
v1.8 版本调整模型思考模式默认行为,并补充了多项设备与平台集成能力。
66

77
### YAML 工作流增强
88

9-
- 带有 `name` 的 YAML 步骤可以在后续步骤中通过 `$name``${name}` 复用结果。详见:[YAML 脚本自动化](./automate-with-scripts-in-yaml#复用步骤结果)
109
- Android `runAdbShell` 在 JavaScript API 和 YAML 脚本中都支持 `timeout` 选项。详见:[Android API](./android-api-reference)[YAML 脚本自动化](./automate-with-scripts-in-yaml)
1110

1211
### 设备与平台集成

apps/site/docs/zh/yaml-script-runner.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ npx midscene ./bing-search.yaml
127127

128128
### `.yaml` 中使用环境变量来填入动态值
129129

130-
脚本中可以通过 `${variable-name}` 引用环境变量。
130+
脚本中可以通过 `${variable-name}` 引用环境变量。环境变量会在 YAML 任务执行前完成替换,包括任务正文中的字符串。
131131

132132
```ini filename=.env
133133
topic=weather today

packages/core/src/yaml/player.ts

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,6 @@ import {
6161

6262
const debug = getDebug('yaml-player');
6363

64-
const VARIABLE_FULL_MATCH_RE = /^\$([a-zA-Z_][a-zA-Z0-9_]*)$/;
65-
const VARIABLE_EMBEDDED_RE = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
66-
67-
function deepTransform(
68-
value: unknown,
69-
transform: (val: unknown) => unknown,
70-
): unknown {
71-
if (Array.isArray(value)) {
72-
return value.map((item) => deepTransform(item, transform));
73-
}
74-
75-
if (value !== null && typeof value === 'object') {
76-
const result: Record<string, unknown> = {};
77-
for (const [key, val] of Object.entries(value)) {
78-
result[key] = deepTransform(val, transform);
79-
}
80-
return result;
81-
}
82-
83-
return transform(value);
84-
}
85-
8664
const aiTaskHandlerMap = {
8765
aiQuery: 'aiQuery',
8866
aiNumber: 'aiNumber',
@@ -307,43 +285,14 @@ export class ScriptPlayer<T extends MidsceneYamlScriptEnv> {
307285
}
308286
}
309287

310-
private resolveVariables(value: unknown): unknown {
311-
return deepTransform(value, (val) => {
312-
if (typeof val !== 'string') {
313-
return val;
314-
}
315-
316-
const fullMatch = val.match(VARIABLE_FULL_MATCH_RE);
317-
if (fullMatch) {
318-
const varName = fullMatch[1];
319-
if (!(varName in this.result)) {
320-
throw new Error(`Variable "${varName}" is not defined`);
321-
}
322-
return this.result[varName];
323-
}
324-
325-
return val.replace(VARIABLE_EMBEDDED_RE, (_, varName) => {
326-
if (!(varName in this.result)) {
327-
throw new Error(`Variable "${varName}" is not defined`);
328-
}
329-
const replacement = this.result[varName];
330-
return typeof replacement === 'string'
331-
? replacement
332-
: JSON.stringify(replacement);
333-
});
334-
});
335-
}
336-
337288
async playTask(taskStatus: ScriptPlayerTaskStatus, agent: Agent) {
338289
const { flow } = taskStatus;
339290
assert(flow, 'missing flow in task');
340291

341292
for (const flowItemIndex in flow) {
342293
const currentStep = Number.parseInt(flowItemIndex, 10);
343294
taskStatus.currentStep = currentStep;
344-
const flowItem = this.resolveVariables(
345-
flow[flowItemIndex],
346-
) as RuntimeYamlFlowItem;
295+
const flowItem = flow[flowItemIndex] as RuntimeYamlFlowItem;
347296
const flowItemRecord = flowItem as Record<string, unknown>;
348297

349298
// Skip Finalize action from cache - it's a planning-only marker

packages/core/tests/unit-test/player-action-dispatch.test.ts

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -351,8 +351,8 @@ describe('player action dispatch ordering', () => {
351351
});
352352
});
353353

354-
describe('player variable interpolation', () => {
355-
it('should replace $var with stored result value', async () => {
354+
describe('player task dispatch without runtime result interpolation', () => {
355+
it('should pass $var text through as a literal value', async () => {
356356
const player = createPlayerWithActionSpace([]);
357357
const agent = createMockAgent();
358358
player.result.product_id = '110';
@@ -369,32 +369,11 @@ describe('player action dispatch ordering', () => {
369369

370370
expect(agent.callActionInActionSpace).toHaveBeenCalledWith(
371371
'Input',
372-
expect.objectContaining({ value: '110' }),
372+
expect.objectContaining({ value: '$product_id' }),
373373
);
374374
});
375375

376-
it('should preserve non-string types for $var replacement', async () => {
377-
const player = createPlayerWithActionSpace([]);
378-
const agent = createMockAgent();
379-
player.result.count = 42;
380-
381-
const taskStatus = {
382-
name: 'test',
383-
flow: [{ aiInput: 'qty field', value: '$count' }],
384-
index: 0,
385-
status: 'running' as const,
386-
totalSteps: 1,
387-
};
388-
389-
await player.playTask(taskStatus, agent);
390-
391-
expect(agent.callActionInActionSpace).toHaveBeenCalledWith(
392-
'Input',
393-
expect.objectContaining({ value: '42' }),
394-
);
395-
});
396-
397-
it('should interpolate ${var} inside strings', async () => {
376+
it('should pass ${var} text through as a literal value', async () => {
398377
const player = createPlayerWithActionSpace([]);
399378
const agent = createMockAgent({
400379
aiQuery: vi.fn().mockResolvedValue('query-result'),
@@ -412,12 +391,12 @@ describe('player action dispatch ordering', () => {
412391
await player.playTask(taskStatus, agent);
413392

414393
expect(agent.aiQuery).toHaveBeenCalledWith(
415-
'search for product-110',
394+
'search for product-${product_id}',
416395
expect.anything(),
417396
);
418397
});
419398

420-
it('should throw when referencing undefined variable', async () => {
399+
it('should not throw when a literal variable-like value is undefined', async () => {
421400
const player = createPlayerWithActionSpace([]);
422401
const agent = createMockAgent();
423402

@@ -429,12 +408,15 @@ describe('player action dispatch ordering', () => {
429408
totalSteps: 1,
430409
};
431410

432-
await expect(player.playTask(taskStatus, agent)).rejects.toThrow(
433-
'Variable "undefined_var" is not defined',
411+
await player.playTask(taskStatus, agent);
412+
413+
expect(agent.callActionInActionSpace).toHaveBeenCalledWith(
414+
'Input',
415+
expect.objectContaining({ value: '$undefined_var' }),
434416
);
435417
});
436418

437-
it('should replace variables in nested objects', async () => {
419+
it('should pass variable-like values through in nested objects', async () => {
438420
const player = createPlayerWithActionSpace([]);
439421
const agent = createMockAgent({
440422
aiTap: vi.fn().mockResolvedValue('tap-result'),
@@ -455,7 +437,10 @@ describe('player action dispatch ordering', () => {
455437

456438
await player.playTask(taskStatus, agent);
457439

458-
expect(agent.aiTap).toHaveBeenCalledWith('search box', expect.anything());
440+
expect(agent.aiTap).toHaveBeenCalledWith(
441+
'$prompt_text',
442+
expect.anything(),
443+
);
459444
});
460445
});
461446

packages/core/tests/unit-test/yaml-doc-usage.test.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe('YAML docs usage coverage', () => {
5151
Reflect.deleteProperty(process.env, 'DOC_HOST');
5252
Reflect.deleteProperty(process.env, 'DOC_TOPIC');
5353
Reflect.deleteProperty(process.env, 'DOC_WIDTH');
54+
Reflect.deleteProperty(process.env, 'product_id');
5455
});
5556

5657
it('parses the documented environment and agent sections', () => {
@@ -328,7 +329,7 @@ tasks:
328329
);
329330
});
330331

331-
it('supports documented result reuse with full-field and embedded interpolation', async () => {
332+
it('keeps named step results in output', async () => {
332333
const script = parseYamlScript(`
333334
web:
334335
url: about:blank
@@ -352,7 +353,7 @@ tasks:
352353

353354
expect(player.status).toBe('done');
354355
expect(agent.callActionInActionSpace).toHaveBeenCalledWith('Input', {
355-
value: 'SKU-123',
356+
value: '$product_id',
356357
locate: {
357358
prompt: 'Search box',
358359
deepLocate: false,
@@ -361,9 +362,14 @@ tasks:
361362
},
362363
});
363364
expect(agent.aiQuery).toHaveBeenCalledWith(
364-
'Get search results after submitting product id SKU-123',
365+
'Get search results after submitting product id ${product_id}',
365366
{},
366367
);
368+
expect(player.result.product_id).toBe('SKU-123');
369+
expect(player.result.search_result).toEqual({
370+
id: 'SKU-123',
371+
title: 'doc item',
372+
});
367373
});
368374

369375
it('continues to the next task when documented task continueOnError is enabled', async () => {
@@ -469,7 +475,7 @@ tasks:
469475
);
470476
});
471477

472-
it('keeps runtime result interpolation in tasks without hiding missing env vars in config', () => {
478+
it('keeps unresolved task environment-variable references literal without hiding missing env vars in config', () => {
473479
expect(() =>
474480
parseYamlScript(`
475481
web:
@@ -485,7 +491,7 @@ tasks:
485491
web:
486492
url: about:blank
487493
tasks:
488-
- name: Runtime interpolation
494+
- name: Literal task reference
489495
flow:
490496
- aiString: Read the product id
491497
name: product_id
@@ -511,7 +517,7 @@ web:
511517
agent:
512518
generateReport: \${DOC_ENABLED}
513519
tasks:
514-
- name: Runtime interpolation
520+
- name: Environment interpolation
515521
flow:
516522
- ai: Search for \${DOC_TOPIC}
517523
- aiQuery: Search for \${product_id}
@@ -529,6 +535,47 @@ tasks:
529535
});
530536
});
531537

538+
it('interpolates environment variables in task strings before execution', async () => {
539+
process.env.product_id = 'ENV-123';
540+
541+
const script = parseYamlScript(`
542+
web:
543+
url: about:blank
544+
tasks:
545+
- name: Environment interpolation conflict
546+
flow:
547+
- aiString: Read the product id
548+
name: product_id
549+
- aiQuery: Search for \${product_id}
550+
name: result
551+
`);
552+
553+
expect(script.tasks[0].flow[1]).toMatchObject({
554+
aiQuery: 'Search for ENV-123',
555+
});
556+
557+
const agent = createDocAgent({
558+
aiString: vi.fn(async () => 'RUNTIME-123'),
559+
aiQuery: vi.fn(async () => 'search-result'),
560+
});
561+
const player = new ScriptPlayer(
562+
script,
563+
async () => ({ agent, freeFn: [] }),
564+
undefined,
565+
);
566+
567+
await player.playTask(
568+
{
569+
...script.tasks[0],
570+
status: 'running',
571+
totalSteps: script.tasks[0].flow.length,
572+
},
573+
agent,
574+
);
575+
576+
expect(agent.aiQuery).toHaveBeenCalledWith('Search for ENV-123', {});
577+
});
578+
532579
it('preserves YAML scalar semantics when interpolating config environment variables', () => {
533580
process.env.DOC_ENABLED = 'true';
534581
process.env.DOC_WIDTH = '1280';
@@ -542,7 +589,7 @@ web:
542589
agent:
543590
generateReport: ${docEnabledRef}
544591
tasks:
545-
- name: Runtime interpolation
592+
- name: Environment interpolation
546593
flow:
547594
- aiQuery: Search for ${docEnabledRef}
548595
`);

0 commit comments

Comments
 (0)