@crosshands/cli 0.2.0 → 0.2.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.
@@ -1,8 +1,82 @@
1
+ import type { Suggestion } from '@crosshands/contract';
2
+ import { type JevHttpStats } from './http.js';
3
+ export type JevPhase = 'observe' | 'bind' | 'named';
4
+ type JevShared = {
5
+ callId: string;
6
+ operation: string;
7
+ snapshotId: string;
8
+ phase: JevPhase;
9
+ goal?: string;
10
+ };
11
+ export type JevCallRecord = {
12
+ kind: 'jev.call';
13
+ callId: string;
14
+ operation: string;
15
+ durationMs: number;
16
+ httpMs: number;
17
+ brokerCalls: number;
18
+ ranks: number;
19
+ phase?: JevPhase;
20
+ bound?: boolean;
21
+ error?: string;
22
+ goal?: string;
23
+ };
24
+ export type JevHttpRecord = JevShared & {
25
+ kind: 'jev.http';
26
+ status: number;
27
+ durationMs: number;
28
+ requestBytes: number;
29
+ responseBytes: number;
30
+ candidateCount: number;
31
+ };
32
+ export type JevDecisionRecord = JevShared & {
33
+ kind: 'jev.decision';
34
+ app?: string;
35
+ move: Suggestion['move']['kind'];
36
+ elementIndex?: number;
37
+ reason?: string;
38
+ confidence?: number;
39
+ goalChars: number;
40
+ treeChars: number;
41
+ elementCount: number;
42
+ candidateCount: number;
43
+ parseMs: number;
44
+ evaluateMs?: number;
45
+ };
46
+ export type JevRecord = JevCallRecord | JevHttpRecord | JevDecisionRecord;
1
47
  export type JevLogger = {
2
- emit(record: Record<string, unknown>): void;
48
+ emit(record: JevRecord): void;
3
49
  close(): Promise<void>;
4
50
  };
51
+ export type JevCall = {
52
+ callId: string;
53
+ operation: string;
54
+ goal?: string;
55
+ ranks: number;
56
+ httpMs: number;
57
+ brokerCalls: number;
58
+ phase?: JevPhase;
59
+ };
60
+ export type JevRankStats = {
61
+ snapshotId: string;
62
+ treeChars: number;
63
+ elementCount: number;
64
+ candidateCount: number;
65
+ parseMs: number;
66
+ goalChars: number;
67
+ evaluateMs?: number;
68
+ app?: string;
69
+ http?: JevHttpStats;
70
+ };
71
+ export type RankRecorder = (phase: JevPhase, stats: JevRankStats, suggestion?: Suggestion) => void;
5
72
  export declare function hashGoal(goal: string): string;
73
+ export declare function createJevCall(operation: string, goal?: string): JevCall;
74
+ export declare function recordRank(log: JevLogger | undefined, call: JevCall | undefined, phase: JevPhase, stats: JevRankStats, suggestion?: Suggestion): void;
75
+ export declare function emitCall(log: JevLogger | undefined, call: JevCall | undefined, started: number, extra?: {
76
+ error?: string;
77
+ bound?: boolean;
78
+ }): void;
6
79
  export declare function createJevLogger(graphicalSessionId: string, env?: NodeJS.ProcessEnv): JevLogger;
7
80
  export declare function createCliJevLogger(env?: NodeJS.ProcessEnv): JevLogger | undefined;
81
+ export {};
8
82
  //# sourceMappingURL=log.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/intent/log.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAID,wBAAgB,eAAe,CAC7B,kBAAkB,EAAE,MAAM,EAC1B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,SAAS,CAoBX;AAED,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,SAAS,GAAG,SAAS,CAG9F"}
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/intent/log.ts"],"names":[],"mappings":"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"}
@@ -2,10 +2,89 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { JsonlFileWriter, diagnosticsDirectory } from '@crosshands/runtime';
3
3
  import { localClientPaths } from '../local-client.js';
4
4
  import { parseJevEnv } from './env.js';
5
+ import { elapsedMs } from './http.js';
5
6
  export function hashGoal(goal) {
6
7
  return createHash('sha256').update(goal).digest('hex');
7
8
  }
8
9
  const OMIT = new Set(['goal', 'apiKey', 'treeText', 'text', 'value', 'TYPESAFE_API_KEY']);
10
+ export function createJevCall(operation, goal) {
11
+ return {
12
+ callId: randomUUID(),
13
+ operation,
14
+ ...(goal === undefined ? {} : { goal }),
15
+ ranks: 0,
16
+ httpMs: 0,
17
+ brokerCalls: 0
18
+ };
19
+ }
20
+ export function recordRank(log, call, phase, stats, suggestion) {
21
+ if (call !== undefined) {
22
+ call.phase = phase;
23
+ call.ranks += 1;
24
+ if (stats.http !== undefined)
25
+ call.httpMs += stats.http.durationMs;
26
+ }
27
+ if (log === undefined || call === undefined)
28
+ return;
29
+ const shared = {
30
+ callId: call.callId,
31
+ operation: call.operation,
32
+ snapshotId: stats.snapshotId,
33
+ phase,
34
+ ...(call.goal === undefined ? {} : { goal: call.goal })
35
+ };
36
+ if (stats.http !== undefined) {
37
+ log.emit({
38
+ kind: 'jev.http',
39
+ ...shared,
40
+ status: stats.http.status,
41
+ durationMs: stats.http.durationMs,
42
+ requestBytes: stats.http.requestBytes,
43
+ responseBytes: stats.http.responseBytes,
44
+ candidateCount: stats.candidateCount
45
+ });
46
+ }
47
+ if (suggestion === undefined)
48
+ return;
49
+ const move = suggestion.move;
50
+ log.emit({
51
+ kind: 'jev.decision',
52
+ ...shared,
53
+ ...(stats.app === undefined ? {} : { app: stats.app }),
54
+ move: move.kind,
55
+ ...(move.kind === 'click' ||
56
+ move.kind === 'setValue' ||
57
+ move.kind === 'secondary' ||
58
+ move.kind === 'scroll'
59
+ ? { elementIndex: move.elementIndex }
60
+ : {}),
61
+ ...(move.kind === 'blocked' ? { reason: move.reason } : {}),
62
+ ...(suggestion.confidence === undefined ? {} : { confidence: suggestion.confidence }),
63
+ goalChars: stats.goalChars,
64
+ treeChars: stats.treeChars,
65
+ elementCount: stats.elementCount,
66
+ candidateCount: stats.candidateCount,
67
+ parseMs: stats.parseMs,
68
+ ...(stats.evaluateMs === undefined ? {} : { evaluateMs: stats.evaluateMs })
69
+ });
70
+ }
71
+ export function emitCall(log, call, started, extra = {}) {
72
+ if (log === undefined || call === undefined)
73
+ return;
74
+ log.emit({
75
+ kind: 'jev.call',
76
+ callId: call.callId,
77
+ operation: call.operation,
78
+ durationMs: elapsedMs(started),
79
+ httpMs: call.httpMs,
80
+ brokerCalls: call.brokerCalls,
81
+ ranks: call.ranks,
82
+ ...(call.phase === undefined ? {} : { phase: call.phase }),
83
+ ...(extra.bound === undefined ? {} : { bound: extra.bound }),
84
+ ...(extra.error === undefined ? {} : { error: extra.error }),
85
+ ...(call.goal === undefined ? {} : { goal: call.goal })
86
+ });
87
+ }
9
88
  export function createJevLogger(graphicalSessionId, env = process.env) {
10
89
  const writer = new JsonlFileWriter({
11
90
  directory: diagnosticsDirectory(graphicalSessionId, { env }),
@@ -14,17 +93,25 @@ export function createJevLogger(graphicalSessionId, env = process.env) {
14
93
  writer.start();
15
94
  return {
16
95
  emit(record) {
17
- const kind = record.kind;
18
- if (typeof kind !== 'string')
19
- return;
20
- const goal = record.goal;
21
- const rest = { ...record, kind, v: 1, ts: new Date().toISOString() };
22
- for (const key of OMIT)
23
- delete rest[key];
24
- writer.emit({
25
- ...rest,
26
- ...(typeof goal === 'string' ? { goalSha256: hashGoal(goal) } : {})
27
- });
96
+ try {
97
+ const raw = record;
98
+ const goal = raw.goal;
99
+ const rest = {
100
+ ...raw,
101
+ kind: record.kind,
102
+ v: 1,
103
+ ts: new Date().toISOString()
104
+ };
105
+ for (const key of OMIT)
106
+ delete rest[key];
107
+ writer.emit({
108
+ ...rest,
109
+ ...(typeof goal === 'string' ? { goalSha256: hashGoal(goal) } : {})
110
+ });
111
+ }
112
+ catch {
113
+ // Jev logs must not fail computer-use.
114
+ }
28
115
  },
29
116
  close: () => writer.close()
30
117
  };
@@ -1 +1 @@
1
- {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/intent/log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEpD,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAE3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAOtC,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACxD,CAAC;AAED,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAA;AAEzF,MAAM,UAAU,eAAe,CAC7B,kBAA0B,EAC1B,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,SAAS,EAAE,oBAAoB,CAAC,kBAAkB,EAAE,EAAE,GAAG,EAAE,CAAC;QAC5D,UAAU,EAAE,OAAO,UAAU,EAAE,EAAE;KAClC,CAAC,CAAA;IACF,MAAM,CAAC,KAAK,EAAE,CAAA;IACd,OAAO;QACL,IAAI,CAAC,MAAM;YACT,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;YACxB,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,OAAM;YACpC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;YACxB,MAAM,IAAI,GAA4B,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAA;YAC7F,KAAK,MAAM,GAAG,IAAI,IAAI;gBAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAA;YACxC,MAAM,CAAC,IAAI,CAAC;gBACV,GAAG,IAAI;gBACP,GAAG,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACpE,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;KAC5B,CAAA;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACrE,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,SAAS,CAAA;IACrD,OAAO,eAAe,CAAC,gBAAgB,EAAE,CAAC,QAAQ,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAA;AAC7E,CAAC"}
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/intent/log.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crosshands/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Agent-agnostic local computer-use CLI",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -23,8 +23,8 @@
23
23
  "access": "public"
24
24
  },
25
25
  "dependencies": {
26
- "@crosshands/contract": "0.2.0",
27
- "@crosshands/runtime": "0.2.0"
26
+ "@crosshands/runtime": "0.2.1",
27
+ "@crosshands/contract": "0.2.1"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "24.10.1",
@@ -32,9 +32,9 @@
32
32
  "vitest": "4.1.5"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@crosshands/platform-linux": "0.2.0",
36
- "@crosshands/platform-windows": "0.2.0",
37
- "@crosshands/platform-darwin": "0.2.0"
35
+ "@crosshands/platform-darwin": "0.2.1",
36
+ "@crosshands/platform-windows": "0.2.1",
37
+ "@crosshands/platform-linux": "0.2.1"
38
38
  },
39
39
  "engines": {
40
40
  "node": ">=22"
@@ -18,12 +18,22 @@ import {
18
18
  import { unwrapBrokerResult } from '../broker-result.js'
19
19
  import { parseJevEnv, type JevEnv } from './env.js'
20
20
  import {
21
+ JevEvaluateError,
21
22
  liveEvaluator,
22
23
  namedTargetAllowed,
23
24
  suggestionFromAnswers,
24
25
  type EvaluateFn
25
26
  } from './evaluate.js'
26
- import type { JevLogger } from './log.js'
27
+ import { elapsedMs } from './http.js'
28
+ import {
29
+ createJevCall,
30
+ emitCall,
31
+ recordRank,
32
+ type JevCall,
33
+ type JevLogger,
34
+ type JevRankStats,
35
+ type RankRecorder
36
+ } from './log.js'
27
37
  import { clickableMoves, parseTreeMoves } from './tree.js'
28
38
 
29
39
  export type IntentBroker = {
@@ -38,6 +48,10 @@ export type DispatchOptions = {
38
48
 
39
49
  const NAMED_GATE = new Set<ComputerOperationName>(['click', 'setValue'])
40
50
 
51
+ type RankOutcome =
52
+ | { ok: true; suggestion: Suggestion; stats: JevRankStats }
53
+ | { ok: false; stats: JevRankStats }
54
+
41
55
  function policyUnavailable() {
42
56
  return createComputerError('policy_unavailable', 'Jev could not rank this snapshot').toJSON()
43
57
  }
@@ -90,40 +104,81 @@ function boundIndex(operation: ComputerOperationName, suggestion: Suggestion): n
90
104
  return undefined
91
105
  }
92
106
 
107
+ function errorFromResult(result: unknown): string | undefined {
108
+ const mutation = PublicMutationResultSchema.safeParse(result)
109
+ if (mutation.success) {
110
+ const outcome = mutation.data.outcome
111
+ if (outcome.state === 'not_attempted' || outcome.state === 'failed') return outcome.error?.code
112
+ return undefined
113
+ }
114
+ const snapshot = PublicSnapshotResultSchema.safeParse(result)
115
+ if (!snapshot.success) return undefined
116
+ return snapshot.data.issues.find((issue) => issue.code === 'policy_unavailable')?.code
117
+ }
118
+
119
+ function boundFromCall(call: JevCall | undefined, result: unknown): boolean | undefined {
120
+ if (call?.phase !== 'bind') return undefined
121
+ const mutation = PublicMutationResultSchema.safeParse(result)
122
+ return mutation.success && mutation.data.resolvedTarget !== undefined
123
+ }
124
+
93
125
  async function rank(
94
126
  look: SnapshotResult,
95
127
  goal: string,
96
128
  evaluate: EvaluateFn,
97
- log: JevLogger | undefined,
98
129
  app?: string
99
- ): Promise<Suggestion> {
130
+ ): Promise<RankOutcome> {
131
+ const snapshotId = look.snapshot.id
132
+ const parseStarted = Date.now()
100
133
  const moves = clickableMoves(parseTreeMoves(look.snapshot.treeText))
134
+ const stats: JevRankStats = {
135
+ snapshotId,
136
+ treeChars: look.snapshot.treeText.length,
137
+ elementCount: look.snapshot.elementCount,
138
+ candidateCount: moves.length,
139
+ parseMs: elapsedMs(parseStarted),
140
+ goalChars: goal.length,
141
+ app: look.snapshot.app.id
142
+ }
101
143
  if (moves.length === 0) {
102
144
  return {
103
- untrusted: true,
104
- snapshotId: look.snapshot.id,
105
- move: { kind: 'blocked', reason: 'no_candidate' }
145
+ ok: true,
146
+ suggestion: {
147
+ untrusted: true,
148
+ snapshotId,
149
+ move: { kind: 'blocked', reason: 'no_candidate' }
150
+ },
151
+ stats
152
+ }
153
+ }
154
+ const evalStarted = Date.now()
155
+ try {
156
+ const evaluated = await evaluate({
157
+ goal,
158
+ ...(app === undefined ? {} : { app }),
159
+ moves
160
+ })
161
+ return {
162
+ ok: true,
163
+ suggestion: suggestionFromAnswers(snapshotId, moves, evaluated.answers),
164
+ stats: {
165
+ ...stats,
166
+ evaluateMs: elapsedMs(evalStarted),
167
+ ...(evaluated.http === undefined ? {} : { http: evaluated.http })
168
+ }
169
+ }
170
+ } catch (cause) {
171
+ if (cause instanceof ComputerError) throw cause
172
+ const http = cause instanceof JevEvaluateError ? cause.http : undefined
173
+ return {
174
+ ok: false,
175
+ stats: {
176
+ ...stats,
177
+ evaluateMs: elapsedMs(evalStarted),
178
+ ...(http === undefined ? {} : { http })
179
+ }
106
180
  }
107
181
  }
108
- const suggestion = suggestionFromAnswers(
109
- look.snapshot.id,
110
- moves,
111
- await evaluate({ goal, ...(app === undefined ? {} : { app }), moves })
112
- )
113
- log?.emit({
114
- kind: 'jev.decision',
115
- snapshotId: suggestion.snapshotId,
116
- move: suggestion.move.kind,
117
- ...(suggestion.move.kind === 'click' ||
118
- suggestion.move.kind === 'setValue' ||
119
- suggestion.move.kind === 'secondary' ||
120
- suggestion.move.kind === 'scroll'
121
- ? { elementIndex: suggestion.move.elementIndex }
122
- : {}),
123
- confidence: suggestion.confidence,
124
- goal
125
- })
126
- return suggestion
127
182
  }
128
183
 
129
184
  function evaluatorFor(env: JevEnv, override: EvaluateFn | undefined): EvaluateFn | undefined {
@@ -144,7 +199,7 @@ async function suggestObserve(
144
199
  goal: string | undefined,
145
200
  env: JevEnv,
146
201
  evaluate: EvaluateFn | undefined,
147
- log: JevLogger | undefined
202
+ record: RankRecorder
148
203
  ): Promise<unknown> {
149
204
  const result = await request('getAppState', toBrokerInput('getAppState', input))
150
205
  if (goal === undefined || env.kind === 'off') return result
@@ -152,10 +207,15 @@ async function suggestObserve(
152
207
  if (env.kind === 'fail_closed' || evaluate === undefined) {
153
208
  return withSnapshotIssue(look, policyUnavailable())
154
209
  }
210
+ const app = 'app' in input && typeof input.app === 'string' ? input.app : undefined
155
211
  try {
156
- const app = 'app' in input && typeof input.app === 'string' ? input.app : undefined
157
- const suggestion = await rank(look, goal, evaluate, log, app)
158
- return parsePublicOutput('getAppState', { ...look, suggestion })
212
+ const ranked = await rank(look, goal, evaluate, app)
213
+ if (!ranked.ok) {
214
+ record('observe', ranked.stats)
215
+ return withSnapshotIssue(look, policyUnavailable())
216
+ }
217
+ record('observe', ranked.stats, ranked.suggestion)
218
+ return parsePublicOutput('getAppState', { ...look, suggestion: ranked.suggestion })
159
219
  } catch {
160
220
  return withSnapshotIssue(look, policyUnavailable())
161
221
  }
@@ -168,7 +228,7 @@ async function bindIntentTarget(
168
228
  goal: string | undefined,
169
229
  env: JevEnv,
170
230
  evaluate: EvaluateFn | undefined,
171
- log: JevLogger | undefined
231
+ record: RankRecorder
172
232
  ): Promise<unknown> {
173
233
  if (env.kind === 'off') {
174
234
  throw createComputerError('invalid_argument', 'intent targeting requires CROSSHANDS_JEV=1')
@@ -181,16 +241,20 @@ async function bindIntentTarget(
181
241
  throw createComputerError('invalid_argument', 'intent targeting requires a context token')
182
242
  }
183
243
  let look: SnapshotResult
184
- let suggestion: Suggestion
185
244
  try {
186
245
  look = await lookAgain(request, token)
187
- suggestion = await rank(look, goal, evaluate, log)
188
246
  } catch (cause) {
189
247
  if (cause instanceof ComputerError) throw cause
190
248
  return notAttempted(policyUnavailable())
191
249
  }
192
- const index = boundIndex(operation, suggestion)
193
- if (index === undefined) return notAttempted(policyUnavailable(), suggestion)
250
+ const ranked = await rank(look, goal, evaluate)
251
+ if (!ranked.ok) {
252
+ record('bind', ranked.stats)
253
+ return notAttempted(policyUnavailable())
254
+ }
255
+ record('bind', ranked.stats, ranked.suggestion)
256
+ const index = boundIndex(operation, ranked.suggestion)
257
+ if (index === undefined) return notAttempted(policyUnavailable(), ranked.suggestion)
194
258
  const bound = {
195
259
  ...splitPublicInput(input).rest,
196
260
  contextToken: look.context.token,
@@ -201,7 +265,7 @@ async function bindIntentTarget(
201
265
  return parsePublicOutput(operation, {
202
266
  ...body,
203
267
  resolvedTarget: { kind: 'element', elementIndex: index },
204
- suggestion
268
+ suggestion: ranked.suggestion
205
269
  })
206
270
  }
207
271
 
@@ -210,25 +274,30 @@ async function gateNamedTarget(
210
274
  input: object,
211
275
  goal: string,
212
276
  evaluate: EvaluateFn,
213
- log: JevLogger | undefined
277
+ record: RankRecorder
214
278
  ): Promise<unknown | undefined> {
215
279
  const named = elementIndexOf(input)
216
280
  const token = contextTokenOf(input)
217
281
  if (token === undefined || named === undefined) return undefined
218
282
  try {
219
283
  const look = await lookAgain(request, token)
220
- const suggestion = await rank(look, goal, evaluate, log)
284
+ const ranked = await rank(look, goal, evaluate)
285
+ if (!ranked.ok) {
286
+ record('named', ranked.stats)
287
+ return notAttempted(policyUnavailable())
288
+ }
289
+ record('named', ranked.stats, ranked.suggestion)
221
290
  const picked =
222
- suggestion.move.kind === 'click' || suggestion.move.kind === 'setValue'
223
- ? suggestion.move.elementIndex
291
+ ranked.suggestion.move.kind === 'click' || ranked.suggestion.move.kind === 'setValue'
292
+ ? ranked.suggestion.move.elementIndex
224
293
  : undefined
225
- if (picked !== named || !namedTargetAllowed(suggestion.confidence)) {
294
+ if (picked !== named || !namedTargetAllowed(ranked.suggestion.confidence)) {
226
295
  return notAttempted(
227
296
  createComputerError(
228
297
  'goal_mismatch',
229
298
  'Named target does not match the per-call goal'
230
299
  ).toJSON(),
231
- suggestion
300
+ ranked.suggestion
232
301
  )
233
302
  }
234
303
  } catch {
@@ -247,30 +316,58 @@ export async function dispatchPublicOperation(
247
316
  const parsed = parsePublicInput(operation, rawInput)
248
317
  const { goal } = splitPublicInput(parsed)
249
318
  const evaluate = evaluatorFor(envState, options.evaluate)
250
- const request: IntentBroker['request'] = async (name, input) =>
251
- unwrapBrokerResult(await broker.request(name, input))
252
-
253
- if (operation === 'getAppState') {
254
- return suggestObserve(
255
- request,
256
- parsed as PublicOperationInput<'getAppState'>,
257
- goal,
258
- envState,
259
- evaluate,
260
- options.log
261
- )
319
+ const log = options.log
320
+ const call = log === undefined ? undefined : createJevCall(operation, goal)
321
+ const record: RankRecorder = (phase, stats, suggestion) => {
322
+ recordRank(log, call, phase, stats, suggestion)
262
323
  }
263
- if (hasIntentTarget(parsed)) {
264
- return bindIntentTarget(request, operation, parsed, goal, envState, evaluate, options.log)
324
+ const request: IntentBroker['request'] = async (name, input) => {
325
+ if (call !== undefined) call.brokerCalls += 1
326
+ return unwrapBrokerResult(await broker.request(name, input))
265
327
  }
266
- if (
267
- goal !== undefined &&
268
- envState.kind === 'on' &&
269
- evaluate !== undefined &&
270
- NAMED_GATE.has(operation)
271
- ) {
272
- const gated = await gateNamedTarget(request, parsed, goal, evaluate, options.log)
273
- if (gated !== undefined) return gated
328
+
329
+ async function dispatch(): Promise<unknown> {
330
+ if (operation === 'getAppState') {
331
+ return suggestObserve(
332
+ request,
333
+ parsed as PublicOperationInput<'getAppState'>,
334
+ goal,
335
+ envState,
336
+ evaluate,
337
+ record
338
+ )
339
+ }
340
+ if (hasIntentTarget(parsed)) {
341
+ return bindIntentTarget(request, operation, parsed, goal, envState, evaluate, record)
342
+ }
343
+ if (
344
+ goal !== undefined &&
345
+ envState.kind === 'on' &&
346
+ evaluate !== undefined &&
347
+ NAMED_GATE.has(operation)
348
+ ) {
349
+ const gated = await gateNamedTarget(request, parsed, goal, evaluate, record)
350
+ if (gated !== undefined) return gated
351
+ }
352
+ return request(operation, toBrokerInput(operation, parsed))
353
+ }
354
+
355
+ const started = Date.now()
356
+ try {
357
+ const result = await dispatch()
358
+ if (call !== undefined) {
359
+ const extra: { error?: string; bound?: boolean } = {}
360
+ const error = errorFromResult(result)
361
+ const bound = boundFromCall(call, result)
362
+ if (error !== undefined) extra.error = error
363
+ if (bound !== undefined) extra.bound = bound
364
+ emitCall(log, call, started, extra)
365
+ }
366
+ return result
367
+ } catch (cause) {
368
+ if (call !== undefined) {
369
+ emitCall(log, call, started, cause instanceof ComputerError ? { error: cause.code } : {})
370
+ }
371
+ throw cause
274
372
  }
275
- return request(operation, toBrokerInput(operation, parsed))
276
373
  }