mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
0edefebe06
## Summary Fixes telemetry attribution for tools dispatched by orchestrators. `run-sequence`, `flow-execute`, and `flow-add-step` run their steps via `registry.invokeTool()` directly, which minted a fresh, unregistered invocation id per nested call — so the `ai_client` / `platform` the HTTP layer captured for the outer request never reached the nested gestures, and they were recorded as **anonymous** in telemetry. Remove `opt_out` event that is a leftover. ## Changes - Add an optional `recordChildInvocation` recorder to `InvokeToolOptions` / `ToolContext`, bound by the HTTP layer to the outer request's attribution. - New `invokeSubTool` helper: registers each sub-tool under a minted id inheriting the request's AI client, with the platform re-derived from the sub-tool's **own** device arg (`udid` / `device_id` / `avdName`); forwards the recorder so propagation survives arbitrary nesting (e.g. `flow-execute → run-sequence → gesture-tap`). - Route the three orchestrators' sub-dispatch through - Refactor platform inference in the HTTP layer into a shared `platformFromArgs` helper (also hardened to not throw on a malformed device arg). - Remove opt_out event leftover Why re-derive platform per child: an orchestrator like `flow-execute` has no platform of its own and a single flow can target several devices, so the child's device arg is the only correct platform source; the parent's platform is the fallback.
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import type { Registry, ToolContext } from "@argent/registry";
|
|
import { invokeSubTool } from "../src/utils/sub-invoke";
|
|
|
|
function mockRegistry(impl?: (id: string, args: unknown) => unknown): Registry {
|
|
return {
|
|
invokeTool: vi.fn(async (id: string, args: unknown) => impl?.(id, args)),
|
|
} as unknown as Registry;
|
|
}
|
|
|
|
describe("invokeSubTool", () => {
|
|
it("invokes directly (no third arg) when there is no telemetry context", async () => {
|
|
const registry = mockRegistry(() => ({ ok: true }));
|
|
|
|
const result = await invokeSubTool(registry, undefined, "gesture-tap", { x: 0.5, y: 0.3 });
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
// No options object — preserves the pre-fix call shape for direct invokes.
|
|
expect(registry.invokeTool).toHaveBeenCalledWith("gesture-tap", { x: 0.5, y: 0.3 });
|
|
});
|
|
|
|
it("invokes directly when the context carries no recorder", async () => {
|
|
const registry = mockRegistry();
|
|
const ctx = { artifacts: {} } as unknown as ToolContext;
|
|
|
|
await invokeSubTool(registry, ctx, "gesture-tap", { x: 0.1 });
|
|
|
|
expect(registry.invokeTool).toHaveBeenCalledWith("gesture-tap", { x: 0.1 });
|
|
});
|
|
|
|
it("records a child invocation and forwards the id + recorder when attribution is present", async () => {
|
|
const registry = mockRegistry(() => ({ done: true }));
|
|
const release = vi.fn();
|
|
const recordChildInvocation = vi.fn((_id: string, _args?: unknown) => release);
|
|
const ctx = { artifacts: {}, recordChildInvocation } as unknown as ToolContext;
|
|
|
|
await invokeSubTool(registry, ctx, "gesture-swipe", { fromX: 0.5 });
|
|
|
|
expect(recordChildInvocation).toHaveBeenCalledOnce();
|
|
const childId = recordChildInvocation.mock.calls[0]![0];
|
|
expect(childId).toEqual(expect.any(String));
|
|
|
|
// The child's own args are handed to the recorder so it can re-derive this
|
|
// sub-tool's platform instead of inheriting the orchestrator's.
|
|
expect(recordChildInvocation).toHaveBeenCalledWith(childId, { fromX: 0.5 });
|
|
|
|
// The sub-tool is invoked under the freshly-minted id, and the recorder is
|
|
// forwarded so propagation survives further nesting.
|
|
expect(registry.invokeTool).toHaveBeenCalledWith(
|
|
"gesture-swipe",
|
|
{ fromX: 0.5 },
|
|
{
|
|
toolInvocationId: childId,
|
|
recordChildInvocation,
|
|
}
|
|
);
|
|
expect(release).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("mints a distinct id per call", async () => {
|
|
const registry = mockRegistry();
|
|
const recordChildInvocation = vi.fn((_id: string) => vi.fn());
|
|
const ctx = { artifacts: {}, recordChildInvocation } as unknown as ToolContext;
|
|
|
|
await invokeSubTool(registry, ctx, "gesture-tap", {});
|
|
await invokeSubTool(registry, ctx, "gesture-tap", {});
|
|
|
|
const idA = recordChildInvocation.mock.calls[0]![0];
|
|
const idB = recordChildInvocation.mock.calls[1]![0];
|
|
expect(idA).not.toEqual(idB);
|
|
});
|
|
|
|
it("releases the recorded metadata even when the sub-tool throws", async () => {
|
|
const registry = {
|
|
invokeTool: vi.fn(async () => {
|
|
throw new Error("boom");
|
|
}),
|
|
} as unknown as Registry;
|
|
const release = vi.fn();
|
|
const recordChildInvocation = vi.fn((_id: string) => release);
|
|
const ctx = { artifacts: {}, recordChildInvocation } as unknown as ToolContext;
|
|
|
|
await expect(invokeSubTool(registry, ctx, "gesture-tap", {})).rejects.toThrow("boom");
|
|
expect(release).toHaveBeenCalledOnce();
|
|
});
|
|
});
|