dsh-plugin-mobile-gateway 0.7.3 → 0.7.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/PROTOCOL.md +15 -13
- package/README.md +9 -4
- package/docs/dsh-0.1.5-rc.2-compatibility-audit.md +255 -0
- package/docs/dsh-rc2-mobile-integration.md +155 -0
- package/docs/typert-remote-gateway-feature-checklist.md +19 -21
- package/lib/dsh-host-adapter.mjs +58 -82
- package/lib/index.mjs +188 -73
- package/lib/session-follower.mjs +136 -0
- package/lib/wire-json.mjs +7 -0
- package/package.json +3 -3
package/lib/dsh-host-adapter.mjs
CHANGED
|
@@ -13,88 +13,52 @@ function requireArray(value, endpoint) {
|
|
|
13
13
|
return value
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
export const DSH_VERSION = '0.1.5-rc.2'
|
|
17
|
+
export const SESSION_FORMAT_VERSION = 3
|
|
18
|
+
|
|
19
|
+
export function readHistoryRecords(records) {
|
|
20
|
+
return requireArray(records, 'session history').map(record => {
|
|
21
|
+
const value = requireRecord(record, 'session history record')
|
|
22
|
+
if (value.type !== 'event') throw new Error('DSH 0.1.5-rc.2 history requires event records')
|
|
23
|
+
const event = requireRecord(value.event, 'session history event')
|
|
24
|
+
if (!Number.isSafeInteger(event.seq) || event.seq < 0) throw new Error('invalid history sequence')
|
|
25
|
+
return event
|
|
26
|
+
})
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
? requireArray(data.args, event.type)
|
|
29
|
-
: requireArray(data.texts, event.type)
|
|
30
|
-
const gaps = requireArray(data.dt, event.type)
|
|
31
|
-
if (gaps.length !== Math.max(0, values.length - 1)) {
|
|
32
|
-
throw new Error(`${event.type} returned invalid timing data`)
|
|
33
|
-
}
|
|
34
|
-
const events = []
|
|
35
|
-
let time = event.time
|
|
36
|
-
for (let index = 0; index < values.length; index += 1) {
|
|
37
|
-
if (index > 0) time += gaps[index - 1]
|
|
38
|
-
let chunk
|
|
39
|
-
if (event.type === 'chunkrow/text-chunks') {
|
|
40
|
-
chunk = { type: 'text-delta', index: data.index, text: values[index] }
|
|
41
|
-
} else if (event.type === 'chunkrow/reasoning-chunks') {
|
|
42
|
-
chunk = { type: 'reasoning-delta', index: data.index, text: values[index] }
|
|
43
|
-
} else {
|
|
44
|
-
chunk = {
|
|
45
|
-
type: 'tool-call-delta',
|
|
46
|
-
index: data.index,
|
|
47
|
-
id: data.id,
|
|
48
|
-
...(Object.hasOwn(data, 'name') ? { name: data.name } : {}),
|
|
49
|
-
argumentsDelta: values[index],
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
events.push({
|
|
53
|
-
type: 'assistant/chunk',
|
|
54
|
-
seq: event.seq + index,
|
|
55
|
-
time,
|
|
56
|
-
data: { turn: data.turn, step: data.step, chunk },
|
|
29
|
+
export function readSessionSnapshot(frame, sessionId) {
|
|
30
|
+
requireRecord(frame, 'session/follow')
|
|
31
|
+
if (frame.type !== 'snapshot' || !Number.isSafeInteger(frame.cursor) || frame.cursor < -1
|
|
32
|
+
|| frame.header?.id !== sessionId) throw new Error('session/follow returned an invalid snapshot')
|
|
33
|
+
if (frame.header.version !== SESSION_FORMAT_VERSION) {
|
|
34
|
+
throw Object.assign(new Error('mobile-gateway requires DSH 0.1.5-rc.2 Session format 3'), {
|
|
35
|
+
code: 'unsupported-session-format',
|
|
57
36
|
})
|
|
58
37
|
}
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const value = requireRecord(record, 'session history record')
|
|
66
|
-
const event = requireRecord(value.event, 'session history event')
|
|
67
|
-
if (value.type === 'event') {
|
|
68
|
-
events.push(event)
|
|
69
|
-
continue
|
|
70
|
-
}
|
|
71
|
-
if (value.type !== 'chunks' || ![
|
|
72
|
-
'chunkrow/text-chunks',
|
|
73
|
-
'chunkrow/reasoning-chunks',
|
|
74
|
-
'chunkrow/tool-call-chunks',
|
|
75
|
-
].includes(event.type)) {
|
|
76
|
-
throw new Error('session history returned an unsupported record')
|
|
77
|
-
}
|
|
78
|
-
if (chunkRowLength(event) === 0) throw new Error(`${event.type} returned an empty run`)
|
|
79
|
-
events.push(...expandChunkRow(event))
|
|
38
|
+
return {
|
|
39
|
+
events: readHistoryRecords(frame.records).map(event => ({ event })),
|
|
40
|
+
hasMore: frame.hasMore === true,
|
|
41
|
+
projections: requireRecord(frame.projections, 'session/follow projections'),
|
|
42
|
+
historyFormatVersion: frame.header.version,
|
|
43
|
+
cursor: frame.cursor,
|
|
80
44
|
}
|
|
81
|
-
return events
|
|
82
45
|
}
|
|
83
46
|
|
|
84
47
|
async function firstStreamFrame(gateway, namespace, method, args, signal) {
|
|
85
|
-
const ownedAbort =
|
|
86
|
-
const streamSignal = signal
|
|
87
|
-
|
|
88
|
-
const iteratorFactory = iterable?.[Symbol.asyncIterator] ?? iterable?.[Symbol.iterator]
|
|
89
|
-
if (typeof iteratorFactory !== 'function') throw new Error(`${namespace}/${method} returned an invalid stream`)
|
|
90
|
-
const iterator = iteratorFactory.call(iterable)
|
|
48
|
+
const ownedAbort = new AbortController()
|
|
49
|
+
const streamSignal = signal ? AbortSignal.any([signal, ownedAbort.signal]) : ownedAbort.signal
|
|
50
|
+
let iterator
|
|
91
51
|
try {
|
|
52
|
+
const iterable = await gateway.stream({ namespace, method, args, signal: streamSignal })
|
|
53
|
+
const iteratorFactory = iterable?.[Symbol.asyncIterator] ?? iterable?.[Symbol.iterator]
|
|
54
|
+
if (typeof iteratorFactory !== 'function') throw new Error(`${namespace}/${method} returned an invalid stream`)
|
|
55
|
+
iterator = iteratorFactory.call(iterable)
|
|
92
56
|
const first = await iterator.next()
|
|
93
57
|
if (first.done) throw new Error(`${namespace}/${method} ended before its opening frame`)
|
|
94
58
|
return first.value
|
|
95
59
|
} finally {
|
|
96
|
-
ownedAbort
|
|
97
|
-
if (typeof iterator
|
|
60
|
+
ownedAbort.abort()
|
|
61
|
+
if (typeof iterator?.return === 'function') await iterator.return()
|
|
98
62
|
}
|
|
99
63
|
}
|
|
100
64
|
|
|
@@ -102,7 +66,7 @@ function requestArgs(request) {
|
|
|
102
66
|
return { request }
|
|
103
67
|
}
|
|
104
68
|
|
|
105
|
-
// The
|
|
69
|
+
// The 0.1.5-rc.2 SessionController names its reserved list argument
|
|
106
70
|
// `_request`. Typert descriptors preserve that source parameter name exactly,
|
|
107
71
|
// so this endpoint cannot share the normal `{ request }` wrapper.
|
|
108
72
|
function sessionListArgs(request) {
|
|
@@ -134,20 +98,19 @@ export function createDshHostAdapter(typertGateway) {
|
|
|
134
98
|
requestArgs(request),
|
|
135
99
|
signal,
|
|
136
100
|
), 'session/follow')
|
|
137
|
-
|
|
138
|
-
throw new Error('session/follow returned an invalid opening snapshot')
|
|
139
|
-
}
|
|
140
|
-
return frame
|
|
101
|
+
return readSessionSnapshot(frame, request.address.sessionId)
|
|
141
102
|
}
|
|
142
103
|
|
|
143
104
|
const history = async (payload, signal) => {
|
|
144
105
|
const request = {
|
|
145
106
|
address: { kind: 'session', sessionId: payload.sessionId },
|
|
146
|
-
|
|
107
|
+
// Older-page requests need only the opening cursor/projections, not another large latest page.
|
|
108
|
+
...(payload.beforeSeq !== undefined ? { maxMessages: 1 }
|
|
109
|
+
: payload.maxMessages === undefined ? {} : { maxMessages: payload.maxMessages }),
|
|
147
110
|
}
|
|
148
111
|
const snapshot = await sessionSnapshot(request, signal)
|
|
149
|
-
let
|
|
150
|
-
let hasMore = snapshot.hasMore
|
|
112
|
+
let events = snapshot.events
|
|
113
|
+
let hasMore = snapshot.hasMore
|
|
151
114
|
if (payload.beforeSeq !== undefined) {
|
|
152
115
|
const page = requireRecord(await invoke('session', 'page', requestArgs({
|
|
153
116
|
address: request.address,
|
|
@@ -155,13 +118,13 @@ export function createDshHostAdapter(typertGateway) {
|
|
|
155
118
|
beforeSeq: payload.beforeSeq,
|
|
156
119
|
...(payload.maxMessages === undefined ? {} : { maxMessages: payload.maxMessages }),
|
|
157
120
|
}), signal), 'session/page')
|
|
158
|
-
|
|
121
|
+
events = readHistoryRecords(page.records).map(event => ({ event }))
|
|
159
122
|
hasMore = page.hasMore === true
|
|
160
123
|
}
|
|
161
124
|
return {
|
|
162
|
-
|
|
125
|
+
...snapshot,
|
|
126
|
+
events,
|
|
163
127
|
hasMore,
|
|
164
|
-
projections: requireRecord(snapshot.projections, 'session/follow projections'),
|
|
165
128
|
}
|
|
166
129
|
}
|
|
167
130
|
|
|
@@ -172,10 +135,13 @@ export function createDshHostAdapter(typertGateway) {
|
|
|
172
135
|
|
|
173
136
|
const commands = {
|
|
174
137
|
list: (sessionId, signal) => invoke('commands', 'list', { agentId: sessionId }, signal),
|
|
175
|
-
|
|
138
|
+
// DSH 0.1.5-rc.2 accepts `submittedAttachments`. The parsed
|
|
139
|
+
// wire images already carry the { type: 'image', ... } shape it expects, so
|
|
140
|
+
// only the field name changes.
|
|
141
|
+
execute: (sessionId, line, attachments, signal) => invoke(
|
|
176
142
|
'commands',
|
|
177
143
|
'execute',
|
|
178
|
-
{ agentId: sessionId, line,
|
|
144
|
+
{ agentId: sessionId, line, submittedAttachments: attachments },
|
|
179
145
|
signal,
|
|
180
146
|
),
|
|
181
147
|
}
|
|
@@ -196,6 +162,8 @@ export function createDshHostAdapter(typertGateway) {
|
|
|
196
162
|
])
|
|
197
163
|
return {
|
|
198
164
|
version: 'remote-gateway',
|
|
165
|
+
dshVersion: DSH_VERSION,
|
|
166
|
+
historyFormatVersion: SESSION_FORMAT_VERSION,
|
|
199
167
|
cwd: os.homedir(),
|
|
200
168
|
attachedSessions: Array.isArray(sessions?.items) ? sessions.items.length : 0,
|
|
201
169
|
canOpenPath: canOpenPath === true,
|
|
@@ -301,6 +269,14 @@ export function createDshHostAdapter(typertGateway) {
|
|
|
301
269
|
describe: (_payload = {}, signal) => describeHost(signal),
|
|
302
270
|
},
|
|
303
271
|
describeHost,
|
|
272
|
+
openSessionStream(sessionId, signal) {
|
|
273
|
+
return typertGateway.stream({
|
|
274
|
+
namespace: 'session', method: 'follow',
|
|
275
|
+
// Bound the opening window at the Host; older records remain available via session.page.
|
|
276
|
+
args: requestArgs({ address: { kind: 'session', sessionId }, maxMessages: 12, assistantStream: true }),
|
|
277
|
+
signal,
|
|
278
|
+
})
|
|
279
|
+
},
|
|
304
280
|
openControlStream(signal) {
|
|
305
281
|
return typertGateway.stream({ namespace: 'session', method: 'control', args: {}, signal })
|
|
306
282
|
},
|