@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.
- package/dist/intent/dispatch.d.ts +1 -1
- package/dist/intent/dispatch.d.ts.map +1 -1
- package/dist/intent/dispatch.js +149 -52
- package/dist/intent/dispatch.js.map +1 -1
- package/dist/intent/evaluate.d.ts +11 -1
- package/dist/intent/evaluate.d.ts.map +1 -1
- package/dist/intent/evaluate.js +101 -60
- package/dist/intent/evaluate.js.map +1 -1
- package/dist/intent/http.d.ts +8 -0
- package/dist/intent/http.d.ts.map +1 -0
- package/dist/intent/http.js +4 -0
- package/dist/intent/http.js.map +1 -0
- package/dist/intent/log.d.ts +75 -1
- package/dist/intent/log.d.ts.map +1 -1
- package/dist/intent/log.js +98 -11
- package/dist/intent/log.js.map +1 -1
- package/package.json +6 -6
- package/src/intent/dispatch.ts +161 -64
- package/src/intent/evaluate.ts +120 -64
- package/src/intent/http.ts +10 -0
- package/src/intent/log.ts +182 -10
package/src/intent/evaluate.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import type { Suggestion } from '@crosshands/contract'
|
|
2
2
|
|
|
3
|
+
import { elapsedMs, type JevHttpStats } from './http.js'
|
|
3
4
|
import type { TreeMove } from './tree.js'
|
|
4
5
|
|
|
6
|
+
export type { JevHttpStats }
|
|
7
|
+
|
|
8
|
+
export class JevEvaluateError extends Error {
|
|
9
|
+
readonly http: JevHttpStats
|
|
10
|
+
constructor(message: string, http: JevHttpStats) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = 'JevEvaluateError'
|
|
13
|
+
this.http = http
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
export type JevAnswers = {
|
|
6
18
|
move: 'click' | 'setValue' | 'wait' | 'done' | 'blocked'
|
|
7
19
|
clickWhich?: string
|
|
@@ -11,13 +23,18 @@ export type JevAnswers = {
|
|
|
11
23
|
blockedReason?: string
|
|
12
24
|
}
|
|
13
25
|
|
|
26
|
+
export type EvaluateResult = {
|
|
27
|
+
answers: JevAnswers
|
|
28
|
+
http?: JevHttpStats
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
export type EvaluateInput = {
|
|
15
32
|
goal: string
|
|
16
33
|
app?: string
|
|
17
34
|
moves: readonly TreeMove[]
|
|
18
35
|
}
|
|
19
36
|
|
|
20
|
-
export type EvaluateFn = (input: EvaluateInput) => Promise<
|
|
37
|
+
export type EvaluateFn = (input: EvaluateInput) => Promise<EvaluateResult>
|
|
21
38
|
|
|
22
39
|
export const GATE_REFUSE_BELOW = 0.35
|
|
23
40
|
|
|
@@ -78,78 +95,117 @@ type SystemOneAnswer = {
|
|
|
78
95
|
confidence?: number
|
|
79
96
|
}
|
|
80
97
|
|
|
98
|
+
const JEV_MODEL = 'jev-latest'
|
|
99
|
+
|
|
100
|
+
function httpStats(
|
|
101
|
+
started: number,
|
|
102
|
+
requestBytes: number,
|
|
103
|
+
status: number,
|
|
104
|
+
responseBytes: number
|
|
105
|
+
): JevHttpStats {
|
|
106
|
+
return {
|
|
107
|
+
status,
|
|
108
|
+
durationMs: elapsedMs(started),
|
|
109
|
+
requestBytes,
|
|
110
|
+
responseBytes
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function answersOf(raw: Record<string, SystemOneAnswer>): JevAnswers {
|
|
115
|
+
const move = raw.move?.choice
|
|
116
|
+
if (
|
|
117
|
+
move !== 'click' &&
|
|
118
|
+
move !== 'setValue' &&
|
|
119
|
+
move !== 'wait' &&
|
|
120
|
+
move !== 'done' &&
|
|
121
|
+
move !== 'blocked'
|
|
122
|
+
) {
|
|
123
|
+
return { move: 'blocked', blockedReason: 'no_candidate' }
|
|
124
|
+
}
|
|
125
|
+
const clickWhich = raw.click_which?.choice
|
|
126
|
+
const setValueWhich = raw.setvalue_which?.choice
|
|
127
|
+
const confidence = raw.move?.confidence ?? raw.click_which?.confidence
|
|
128
|
+
const goalMet = raw.goal_met?.noul
|
|
129
|
+
return {
|
|
130
|
+
move,
|
|
131
|
+
...(clickWhich === undefined ? {} : { clickWhich }),
|
|
132
|
+
...(setValueWhich === undefined ? {} : { setValueWhich }),
|
|
133
|
+
...(confidence === undefined ? {} : { confidence }),
|
|
134
|
+
...(goalMet === undefined ? {} : { goalMet })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
81
138
|
export function liveEvaluator(apiKey: string): EvaluateFn {
|
|
82
139
|
return async (input) => {
|
|
83
140
|
const clickable = criteria(input.moves)
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
141
|
+
const payload = JSON.stringify({
|
|
142
|
+
model: JEV_MODEL,
|
|
143
|
+
state: {
|
|
144
|
+
goal: input.goal,
|
|
145
|
+
...(input.app === undefined ? {} : { app: input.app }),
|
|
146
|
+
clickable
|
|
89
147
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
instructions: 'What should we do next for the goal?',
|
|
101
|
-
criteria: {
|
|
102
|
-
click: 'Press one clickable control',
|
|
103
|
-
setValue: 'Pick a field for the caller to fill',
|
|
104
|
-
wait: 'The window is still loading',
|
|
105
|
-
done: 'The goal is already met',
|
|
106
|
-
blocked: 'Stop'
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
click_which: {
|
|
110
|
-
type: 'choice',
|
|
111
|
-
instructions: 'If the move is click, which control?',
|
|
112
|
-
criteria: clickable
|
|
113
|
-
},
|
|
114
|
-
setvalue_which: {
|
|
115
|
-
type: 'choice',
|
|
116
|
-
instructions: 'If the move is setValue, which field?',
|
|
117
|
-
criteria: clickable
|
|
118
|
-
},
|
|
119
|
-
goal_met: {
|
|
120
|
-
type: 'noul',
|
|
121
|
-
instructions: 'Is the goal already met?'
|
|
148
|
+
questions: {
|
|
149
|
+
move: {
|
|
150
|
+
type: 'choice',
|
|
151
|
+
instructions: 'What should we do next for the goal?',
|
|
152
|
+
criteria: {
|
|
153
|
+
click: 'Press one clickable control',
|
|
154
|
+
setValue: 'Pick a field for the caller to fill',
|
|
155
|
+
wait: 'The window is still loading',
|
|
156
|
+
done: 'The goal is already met',
|
|
157
|
+
blocked: 'Stop'
|
|
122
158
|
}
|
|
159
|
+
},
|
|
160
|
+
click_which: {
|
|
161
|
+
type: 'choice',
|
|
162
|
+
instructions: 'If the move is click, which control?',
|
|
163
|
+
criteria: clickable
|
|
164
|
+
},
|
|
165
|
+
setvalue_which: {
|
|
166
|
+
type: 'choice',
|
|
167
|
+
instructions: 'If the move is setValue, which field?',
|
|
168
|
+
criteria: clickable
|
|
169
|
+
},
|
|
170
|
+
goal_met: {
|
|
171
|
+
type: 'noul',
|
|
172
|
+
instructions: 'Is the goal already met?'
|
|
123
173
|
}
|
|
124
|
-
}
|
|
174
|
+
}
|
|
125
175
|
})
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
176
|
+
const requestBytes = Buffer.byteLength(payload)
|
|
177
|
+
const started = Date.now()
|
|
178
|
+
let status = 0
|
|
179
|
+
let responseBytes = 0
|
|
180
|
+
let raw = ''
|
|
181
|
+
try {
|
|
182
|
+
const response = await fetch('https://api.typesafe.ai/v1/systemone', {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
headers: {
|
|
185
|
+
authorization: `Bearer ${apiKey}`,
|
|
186
|
+
'content-type': 'application/json'
|
|
187
|
+
},
|
|
188
|
+
body: payload
|
|
189
|
+
})
|
|
190
|
+
status = response.status
|
|
191
|
+
raw = await response.text()
|
|
192
|
+
responseBytes = Buffer.byteLength(raw)
|
|
193
|
+
} catch (cause) {
|
|
194
|
+
throw new JevEvaluateError(
|
|
195
|
+
cause instanceof Error ? cause.message : 'TypeSafe HTTP failed',
|
|
196
|
+
httpStats(started, requestBytes, 0, 0)
|
|
197
|
+
)
|
|
131
198
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
move !== 'click' &&
|
|
136
|
-
move !== 'setValue' &&
|
|
137
|
-
move !== 'wait' &&
|
|
138
|
-
move !== 'done' &&
|
|
139
|
-
move !== 'blocked'
|
|
140
|
-
) {
|
|
141
|
-
return { move: 'blocked', blockedReason: 'no_candidate' }
|
|
199
|
+
const http = httpStats(started, requestBytes, status, responseBytes)
|
|
200
|
+
if (status < 200 || status >= 300) {
|
|
201
|
+
throw new JevEvaluateError(`TypeSafe HTTP ${status}`, http)
|
|
142
202
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
move,
|
|
149
|
-
...(clickWhich === undefined ? {} : { clickWhich }),
|
|
150
|
-
...(setValueWhich === undefined ? {} : { setValueWhich }),
|
|
151
|
-
...(confidence === undefined ? {} : { confidence }),
|
|
152
|
-
...(goalMet === undefined ? {} : { goalMet })
|
|
203
|
+
let parsed: { answers?: Record<string, SystemOneAnswer> }
|
|
204
|
+
try {
|
|
205
|
+
parsed = JSON.parse(raw) as { answers?: Record<string, SystemOneAnswer> }
|
|
206
|
+
} catch {
|
|
207
|
+
throw new JevEvaluateError('TypeSafe HTTP response was not JSON', http)
|
|
153
208
|
}
|
|
209
|
+
return { answers: answersOf(parsed.answers ?? {}), http }
|
|
154
210
|
}
|
|
155
211
|
}
|
package/src/intent/log.ts
CHANGED
|
@@ -1,21 +1,185 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto'
|
|
2
2
|
|
|
3
|
+
import type { Suggestion } from '@crosshands/contract'
|
|
3
4
|
import { JsonlFileWriter, diagnosticsDirectory } from '@crosshands/runtime'
|
|
4
5
|
|
|
5
6
|
import { localClientPaths } from '../local-client.js'
|
|
6
7
|
import { parseJevEnv } from './env.js'
|
|
8
|
+
import { elapsedMs, type JevHttpStats } from './http.js'
|
|
9
|
+
|
|
10
|
+
export type JevPhase = 'observe' | 'bind' | 'named'
|
|
11
|
+
|
|
12
|
+
type JevShared = {
|
|
13
|
+
callId: string
|
|
14
|
+
operation: string
|
|
15
|
+
snapshotId: string
|
|
16
|
+
phase: JevPhase
|
|
17
|
+
goal?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type JevCallRecord = {
|
|
21
|
+
kind: 'jev.call'
|
|
22
|
+
callId: string
|
|
23
|
+
operation: string
|
|
24
|
+
durationMs: number
|
|
25
|
+
httpMs: number
|
|
26
|
+
brokerCalls: number
|
|
27
|
+
ranks: number
|
|
28
|
+
phase?: JevPhase
|
|
29
|
+
bound?: boolean
|
|
30
|
+
error?: string
|
|
31
|
+
goal?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type JevHttpRecord = JevShared & {
|
|
35
|
+
kind: 'jev.http'
|
|
36
|
+
status: number
|
|
37
|
+
durationMs: number
|
|
38
|
+
requestBytes: number
|
|
39
|
+
responseBytes: number
|
|
40
|
+
candidateCount: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type JevDecisionRecord = JevShared & {
|
|
44
|
+
kind: 'jev.decision'
|
|
45
|
+
app?: string
|
|
46
|
+
move: Suggestion['move']['kind']
|
|
47
|
+
elementIndex?: number
|
|
48
|
+
reason?: string
|
|
49
|
+
confidence?: number
|
|
50
|
+
goalChars: number
|
|
51
|
+
treeChars: number
|
|
52
|
+
elementCount: number
|
|
53
|
+
candidateCount: number
|
|
54
|
+
parseMs: number
|
|
55
|
+
evaluateMs?: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type JevRecord = JevCallRecord | JevHttpRecord | JevDecisionRecord
|
|
7
59
|
|
|
8
60
|
export type JevLogger = {
|
|
9
|
-
emit(record:
|
|
61
|
+
emit(record: JevRecord): void
|
|
10
62
|
close(): Promise<void>
|
|
11
63
|
}
|
|
12
64
|
|
|
65
|
+
export type JevCall = {
|
|
66
|
+
callId: string
|
|
67
|
+
operation: string
|
|
68
|
+
goal?: string
|
|
69
|
+
ranks: number
|
|
70
|
+
httpMs: number
|
|
71
|
+
brokerCalls: number
|
|
72
|
+
phase?: JevPhase
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type JevRankStats = {
|
|
76
|
+
snapshotId: string
|
|
77
|
+
treeChars: number
|
|
78
|
+
elementCount: number
|
|
79
|
+
candidateCount: number
|
|
80
|
+
parseMs: number
|
|
81
|
+
goalChars: number
|
|
82
|
+
evaluateMs?: number
|
|
83
|
+
app?: string
|
|
84
|
+
http?: JevHttpStats
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type RankRecorder = (phase: JevPhase, stats: JevRankStats, suggestion?: Suggestion) => void
|
|
88
|
+
|
|
13
89
|
export function hashGoal(goal: string): string {
|
|
14
90
|
return createHash('sha256').update(goal).digest('hex')
|
|
15
91
|
}
|
|
16
92
|
|
|
17
93
|
const OMIT = new Set(['goal', 'apiKey', 'treeText', 'text', 'value', 'TYPESAFE_API_KEY'])
|
|
18
94
|
|
|
95
|
+
export function createJevCall(operation: string, goal?: string): JevCall {
|
|
96
|
+
return {
|
|
97
|
+
callId: randomUUID(),
|
|
98
|
+
operation,
|
|
99
|
+
...(goal === undefined ? {} : { goal }),
|
|
100
|
+
ranks: 0,
|
|
101
|
+
httpMs: 0,
|
|
102
|
+
brokerCalls: 0
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function recordRank(
|
|
107
|
+
log: JevLogger | undefined,
|
|
108
|
+
call: JevCall | undefined,
|
|
109
|
+
phase: JevPhase,
|
|
110
|
+
stats: JevRankStats,
|
|
111
|
+
suggestion?: Suggestion
|
|
112
|
+
): void {
|
|
113
|
+
if (call !== undefined) {
|
|
114
|
+
call.phase = phase
|
|
115
|
+
call.ranks += 1
|
|
116
|
+
if (stats.http !== undefined) call.httpMs += stats.http.durationMs
|
|
117
|
+
}
|
|
118
|
+
if (log === undefined || call === undefined) return
|
|
119
|
+
const shared: JevShared = {
|
|
120
|
+
callId: call.callId,
|
|
121
|
+
operation: call.operation,
|
|
122
|
+
snapshotId: stats.snapshotId,
|
|
123
|
+
phase,
|
|
124
|
+
...(call.goal === undefined ? {} : { goal: call.goal })
|
|
125
|
+
}
|
|
126
|
+
if (stats.http !== undefined) {
|
|
127
|
+
log.emit({
|
|
128
|
+
kind: 'jev.http',
|
|
129
|
+
...shared,
|
|
130
|
+
status: stats.http.status,
|
|
131
|
+
durationMs: stats.http.durationMs,
|
|
132
|
+
requestBytes: stats.http.requestBytes,
|
|
133
|
+
responseBytes: stats.http.responseBytes,
|
|
134
|
+
candidateCount: stats.candidateCount
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
if (suggestion === undefined) return
|
|
138
|
+
const move = suggestion.move
|
|
139
|
+
log.emit({
|
|
140
|
+
kind: 'jev.decision',
|
|
141
|
+
...shared,
|
|
142
|
+
...(stats.app === undefined ? {} : { app: stats.app }),
|
|
143
|
+
move: move.kind,
|
|
144
|
+
...(move.kind === 'click' ||
|
|
145
|
+
move.kind === 'setValue' ||
|
|
146
|
+
move.kind === 'secondary' ||
|
|
147
|
+
move.kind === 'scroll'
|
|
148
|
+
? { elementIndex: move.elementIndex }
|
|
149
|
+
: {}),
|
|
150
|
+
...(move.kind === 'blocked' ? { reason: move.reason } : {}),
|
|
151
|
+
...(suggestion.confidence === undefined ? {} : { confidence: suggestion.confidence }),
|
|
152
|
+
goalChars: stats.goalChars,
|
|
153
|
+
treeChars: stats.treeChars,
|
|
154
|
+
elementCount: stats.elementCount,
|
|
155
|
+
candidateCount: stats.candidateCount,
|
|
156
|
+
parseMs: stats.parseMs,
|
|
157
|
+
...(stats.evaluateMs === undefined ? {} : { evaluateMs: stats.evaluateMs })
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function emitCall(
|
|
162
|
+
log: JevLogger | undefined,
|
|
163
|
+
call: JevCall | undefined,
|
|
164
|
+
started: number,
|
|
165
|
+
extra: { error?: string; bound?: boolean } = {}
|
|
166
|
+
): void {
|
|
167
|
+
if (log === undefined || call === undefined) return
|
|
168
|
+
log.emit({
|
|
169
|
+
kind: 'jev.call',
|
|
170
|
+
callId: call.callId,
|
|
171
|
+
operation: call.operation,
|
|
172
|
+
durationMs: elapsedMs(started),
|
|
173
|
+
httpMs: call.httpMs,
|
|
174
|
+
brokerCalls: call.brokerCalls,
|
|
175
|
+
ranks: call.ranks,
|
|
176
|
+
...(call.phase === undefined ? {} : { phase: call.phase }),
|
|
177
|
+
...(extra.bound === undefined ? {} : { bound: extra.bound }),
|
|
178
|
+
...(extra.error === undefined ? {} : { error: extra.error }),
|
|
179
|
+
...(call.goal === undefined ? {} : { goal: call.goal })
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
19
183
|
export function createJevLogger(
|
|
20
184
|
graphicalSessionId: string,
|
|
21
185
|
env: NodeJS.ProcessEnv = process.env
|
|
@@ -27,15 +191,23 @@ export function createJevLogger(
|
|
|
27
191
|
writer.start()
|
|
28
192
|
return {
|
|
29
193
|
emit(record) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
194
|
+
try {
|
|
195
|
+
const raw = record as JevRecord & Record<string, unknown>
|
|
196
|
+
const goal = raw.goal
|
|
197
|
+
const rest: Record<string, unknown> = {
|
|
198
|
+
...raw,
|
|
199
|
+
kind: record.kind,
|
|
200
|
+
v: 1,
|
|
201
|
+
ts: new Date().toISOString()
|
|
202
|
+
}
|
|
203
|
+
for (const key of OMIT) delete rest[key]
|
|
204
|
+
writer.emit({
|
|
205
|
+
...rest,
|
|
206
|
+
...(typeof goal === 'string' ? { goalSha256: hashGoal(goal) } : {})
|
|
207
|
+
})
|
|
208
|
+
} catch {
|
|
209
|
+
// Jev logs must not fail computer-use.
|
|
210
|
+
}
|
|
39
211
|
},
|
|
40
212
|
close: () => writer.close()
|
|
41
213
|
}
|