@jigging/agent-method 0.0.0 → 0.1.0-alpha.3

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.
Files changed (52) hide show
  1. package/AGENTS.md +98 -0
  2. package/FLOW.contract.json +241 -0
  3. package/FLOW.meta.json +8 -0
  4. package/FLOW.ts +3 -0
  5. package/LICENSE +373 -0
  6. package/README.md +369 -0
  7. package/THIRD_PARTY_NOTICES +9 -0
  8. package/contracts/acp-public-updates.json +75 -0
  9. package/contracts/agent-commands.json +88 -0
  10. package/contracts/agent-replies.json +202 -0
  11. package/contracts/http-request/contract.json +37 -0
  12. package/dist/api.d.ts +11 -0
  13. package/dist/api.js +164 -0
  14. package/dist/conversation.d.ts +68 -0
  15. package/dist/conversation.js +346 -0
  16. package/dist/errors.d.ts +5 -0
  17. package/dist/errors.js +8 -0
  18. package/dist/flow.d.ts +3 -0
  19. package/dist/flow.js +2784 -0
  20. package/dist/index.d.ts +67 -0
  21. package/dist/index.js +220 -0
  22. package/dist/json.d.ts +21 -0
  23. package/dist/json.js +409 -0
  24. package/dist/schema.d.ts +7 -0
  25. package/dist/schema.js +180 -0
  26. package/dist/skills.d.ts +3 -0
  27. package/dist/skills.js +132 -0
  28. package/dist/values.d.ts +11 -0
  29. package/dist/values.js +65 -0
  30. package/justfile +32 -0
  31. package/licenses/flow.LICENSE +202 -0
  32. package/package.json +44 -4
  33. package/settings.schema.json +12 -0
  34. package/skills/answer-check/SKILL.md +5 -0
  35. package/src/api.ts +191 -0
  36. package/src/conversation.ts +387 -0
  37. package/src/errors.ts +11 -0
  38. package/src/flow.ts +66 -0
  39. package/src/index.ts +325 -0
  40. package/src/json.ts +406 -0
  41. package/src/schema.ts +230 -0
  42. package/src/skills.ts +138 -0
  43. package/src/values.ts +77 -0
  44. package/test/api.test.ts +326 -0
  45. package/test/conversation-fixture.ts +70 -0
  46. package/test/conversation.test.ts +305 -0
  47. package/test/json.test.ts +88 -0
  48. package/test/method.test.ts +308 -0
  49. package/test/pack.test.ts +81 -0
  50. package/test/result.test.ts +103 -0
  51. package/test/skills-flow.test.ts +252 -0
  52. package/tsconfig.json +17 -0
@@ -0,0 +1,305 @@
1
+ import { expect, test } from 'bun:test'
2
+
3
+ /** Synthetic Agent host, real public SDK subprocess. No native-client qualification. */
4
+ async function exercise(mode: string) {
5
+ const process = Bun.spawn([Bun.which('bun')!, `${import.meta.dir}/conversation-fixture.ts`], {
6
+ stdin: 'pipe',
7
+ stdout: 'pipe',
8
+ stderr: 'pipe',
9
+ })
10
+ const errors = new Response(process.stderr).text()
11
+ const timer = setTimeout(() => process.kill(), 5_000)
12
+ const send = (frame: unknown) => {
13
+ process.stdin.write(JSON.stringify(frame) + '\n')
14
+ process.stdin.flush()
15
+ }
16
+ const ok = (request: any, result: unknown) => send({ jsonrpc: '2.0', id: request.id, result })
17
+ const fail = (request: any, code: string) =>
18
+ send({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: code, data: { code } } })
19
+ const queues: any[] = []
20
+ let read: any
21
+ let eventRead: any
22
+ const eventQueue = [
23
+ { item: { sequence: 1, value: { sessionUpdate: 'plan', entries: [], turn: 0 } } },
24
+ {
25
+ item: {
26
+ sequence: 2,
27
+ value: {
28
+ sessionUpdate: 'agent_message_chunk',
29
+ content: { type: 'text', text: 'visible draft' },
30
+ turn: 0,
31
+ },
32
+ },
33
+ },
34
+ ]
35
+ let call: any
36
+ let sequence = 0
37
+ let creates = 0
38
+ let closed = false
39
+ let final: any
40
+ let nativeSettled = false
41
+ const operations: any[] = []
42
+ const item = (value: unknown) => {
43
+ queues.push({ item: { sequence: ++sequence, value } })
44
+ if (read) {
45
+ ok(read, queues.shift())
46
+ read = undefined
47
+ }
48
+ }
49
+ const terminal = (turn: number, type = 'result') =>
50
+ item(
51
+ type === 'result'
52
+ ? { type, turn, result: { outcome: 'done', output: { text: `answer ${turn}` } } }
53
+ : { type, turn },
54
+ )
55
+ send({
56
+ jsonrpc: '2.0',
57
+ id: 'root',
58
+ method: 'flow/run',
59
+ params: {
60
+ protocol: 'run/0',
61
+ input: mode,
62
+ settings: {},
63
+ attachments: {},
64
+ scratch: '/tmp/unused',
65
+ deadlineUnixMs: Date.now() + 4_000,
66
+ },
67
+ })
68
+ try {
69
+ let buffer = ''
70
+ const decoder = new TextDecoder()
71
+ for await (const bytes of process.stdout) {
72
+ buffer += decoder.decode(bytes, { stream: true })
73
+ while (buffer.includes('\n')) {
74
+ const split = buffer.indexOf('\n')
75
+ const request = JSON.parse(buffer.slice(0, split))
76
+ buffer = buffer.slice(split + 1)
77
+ operations.push(request)
78
+ if (request.id === 'root') {
79
+ final = request
80
+ continue
81
+ }
82
+ const p = request.params
83
+ if (request.method === 'channel/create') {
84
+ creates++
85
+ if (mode === 'partial-allocation' && creates === 2) {
86
+ fail(request, 'RESOURCE_EXHAUSTED')
87
+ continue
88
+ }
89
+ const prefix = creates === 1 ? 'commands' : creates === 2 ? 'replies' : 'events'
90
+ expect(p.contract).toEqual({ slot: 'agent', channel: prefix })
91
+ ok(request, {
92
+ send: { endpoint: `${prefix}:s`, direction: 'send', delivery: 'direct' },
93
+ receive: {
94
+ endpoint: `${prefix}:r`,
95
+ direction: 'receive',
96
+ delivery: 'direct',
97
+ startSequence: 1,
98
+ },
99
+ })
100
+ } else if (request.method === 'flow/call') {
101
+ call = request
102
+ expect(p.channels).toEqual({
103
+ commands: 'commands:r',
104
+ replies: 'replies:s',
105
+ ...(mode.startsWith('observer') ? { events: 'events:s' } : {}),
106
+ })
107
+ expect(p.input.session).toEqual(
108
+ ['retained', 'missing-session', 'invalid-session'].includes(mode)
109
+ ? { retain: true }
110
+ : undefined,
111
+ )
112
+ if (mode === 'unavailable' || mode === 'observer-unavailable') {
113
+ nativeSettled = true
114
+ fail(call, 'UNAVAILABLE')
115
+ } else if (mode === 'root-cancel' || mode === 'observer-root-cancel') {
116
+ send({ jsonrpc: '2.0', method: 'request/cancel', params: { requestId: 'root' } })
117
+ } else if (mode === 'abandoned') {
118
+ // No result: callback returns while a real invocation remains live.
119
+ } else if (mode === 'malformed')
120
+ item({ type: 'result', turn: 7, result: { outcome: 'done', output: null } })
121
+ else terminal(0)
122
+ } else if (request.method === 'channel/next') {
123
+ if (p.endpoint === 'events:r') {
124
+ if (mode === 'observer-lagged') fail(request, 'LAGGED')
125
+ else if (mode === 'observer-disposal') fail(request, 'CANCELLED')
126
+ else if (eventQueue.length) ok(request, eventQueue.shift())
127
+ else if (nativeSettled) ok(request, { end: { lastSequence: 2 } })
128
+ else eventRead = request
129
+ continue
130
+ }
131
+ read = request
132
+ if (queues.length) {
133
+ ok(read, queues.shift())
134
+ read = undefined
135
+ }
136
+ } else if (request.method === 'channel/send') {
137
+ ok(request, null)
138
+ const command = p.value
139
+ item({ type: 'accepted', command: command.type, turn: command.turn })
140
+ if (command.type === 'prompt') {
141
+ if (mode === 'late-error') {
142
+ nativeSettled = true
143
+ fail(call, 'EXECUTION_FAILED')
144
+ } else if (!['interrupt', 'completion-race'].includes(mode)) terminal(command.turn)
145
+ } else if (command.type === 'interrupt')
146
+ terminal(command.turn, mode === 'interrupt' ? 'cancelled' : 'result')
147
+ } else if (request.method === 'channel/close') {
148
+ expect(p.endpoint).toBe('commands:s')
149
+ closed = true
150
+ ok(request, null)
151
+ if (call && !nativeSettled) {
152
+ nativeSettled = true
153
+ if (eventRead) {
154
+ ok(eventRead, { end: { lastSequence: 2 } })
155
+ eventRead = undefined
156
+ }
157
+ if (p.error) fail(call, 'EXECUTION_FAILED')
158
+ else
159
+ ok(call, {
160
+ outcome: 'done',
161
+ output: {
162
+ turns: mode === 'callback-error' ? 1 : 2,
163
+ ...(['retained', 'invalid-session', 'unsolicited-session'].includes(mode)
164
+ ? {
165
+ session: {
166
+ status: 'retained',
167
+ reference:
168
+ mode === 'invalid-session'
169
+ ? 'x'.repeat(36)
170
+ : '013579ab-cdef-4567-89ab-0123456789ab',
171
+ },
172
+ }
173
+ : {}),
174
+ },
175
+ })
176
+ }
177
+ } else if (request.method === 'channel/release') {
178
+ if (p.endpoint === 'events:r') {
179
+ ok(
180
+ request,
181
+ mode === 'observer-disposal'
182
+ ? { status: 'failed', code: 'LAGGED' }
183
+ : { status: 'released' },
184
+ )
185
+ continue
186
+ }
187
+ expect(['replies:r', ...(mode === 'partial-allocation' ? ['commands:r'] : [])]).toContain(
188
+ p.endpoint,
189
+ )
190
+ if (call) expect(nativeSettled).toBe(true)
191
+ if (mode === 'disposal-error') ok(request, { status: 'failed', code: 'LAGGED' })
192
+ else ok(request, { status: 'released' })
193
+ } else if (request.method === 'request/cancel') {
194
+ if (eventRead && eventRead.id === p.requestId) {
195
+ fail(eventRead, 'CANCELLED')
196
+ eventRead = undefined
197
+ } else if (read && read.id === p.requestId) {
198
+ fail(read, 'CANCELLED')
199
+ read = undefined
200
+ } else if (
201
+ (mode === 'root-cancel' || mode === 'observer-root-cancel') &&
202
+ call &&
203
+ call.id === p.requestId
204
+ ) {
205
+ nativeSettled = true
206
+ fail(call, 'CANCELLED')
207
+ } else if (
208
+ operations.some(
209
+ (operation) => operation.id === p.requestId && operation.method === 'channel/next',
210
+ )
211
+ ) {
212
+ // Cancellation may arrive after this fixture already answered the read.
213
+ } else
214
+ throw Error('Helper cancelled its invocation waiter instead of awaiting settlement')
215
+ } else throw Error(`unexpected method ${request.method}`)
216
+ }
217
+ }
218
+ expect(await process.exited, await errors).toBe(0)
219
+ if (mode === 'root-cancel' || mode === 'observer-root-cancel') {
220
+ expect(final?.error?.data?.code).toBe('CANCELLED')
221
+ return { output: null, operations }
222
+ }
223
+ expect(final?.result?.outcome).toBe('done')
224
+ expect(closed).toBe(true)
225
+ return { output: final.result.output, operations }
226
+ } finally {
227
+ clearTimeout(timer)
228
+ process.kill()
229
+ await process.exited
230
+ }
231
+ }
232
+
233
+ for (const mode of ['normal', 'interrupt', 'completion-race']) {
234
+ test(`public conversation helper: ${mode}`, async () => {
235
+ const { output } = await exercise(mode)
236
+ expect(output.turns).toHaveLength(2)
237
+ expect(output.value.second.type).toBe(mode === 'interrupt' ? 'cancelled' : 'result')
238
+ expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 2 } })
239
+ })
240
+ }
241
+ test('helper filters live public events without caller channel coordination', async () => {
242
+ const { output } = await exercise('observer')
243
+ expect(output.displayed).toEqual(['visible draft'])
244
+ expect(output.observation).toEqual({ status: 'complete' })
245
+ expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 2 } })
246
+ })
247
+ for (const mode of ['observer-throws', 'observer-async', 'observer-lagged', 'observer-disposal']) {
248
+ test(`${mode} leaves successful Agent settlement intact with explicit observation loss`, async () => {
249
+ const { output } = await exercise(mode)
250
+ expect(output.observation.status).toBe('incomplete')
251
+ expect(output.observation.errors.length).toBeGreaterThan(0)
252
+ expect(output.turns).toHaveLength(2)
253
+ expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 2 } })
254
+ })
255
+ }
256
+ test('public conversation helper keeps requested retention on final settlement only', async () => {
257
+ const { output } = await exercise('retained')
258
+ expect(output.turns).toHaveLength(2)
259
+ expect(output.turns.every((turn: any) => turn.result.output.session === undefined)).toBe(true)
260
+ expect(output.settlement).toEqual({
261
+ outcome: 'done',
262
+ output: {
263
+ turns: 2,
264
+ session: { status: 'retained', reference: '013579ab-cdef-4567-89ab-0123456789ab' },
265
+ },
266
+ })
267
+ })
268
+ for (const mode of ['missing-session', 'invalid-session', 'unsolicited-session']) {
269
+ test(`public conversation helper refuses ${mode} while retaining settled turns`, async () => {
270
+ const { output } = await exercise(mode)
271
+ expect(output.errors).toContain('Agent omitted matching conversation settlement')
272
+ expect(output.turns).toHaveLength(2)
273
+ expect(output.settlement.output.turns).toBe(2)
274
+ })
275
+ }
276
+
277
+ for (const mode of [
278
+ 'unavailable',
279
+ 'late-error',
280
+ 'malformed',
281
+ 'partial-allocation',
282
+ 'callback-error',
283
+ 'abandoned',
284
+ 'disposal-error',
285
+ ]) {
286
+ test(`public conversation helper retains honest ${mode}`, async () => {
287
+ const { output } = await exercise(mode)
288
+ expect(output.errors.length).toBeGreaterThan(0)
289
+ expect(output.turns).toHaveLength(
290
+ mode === 'disposal-error' ? 2 : ['late-error', 'callback-error'].includes(mode) ? 1 : 0,
291
+ )
292
+ if (mode === 'callback-error')
293
+ expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 1 } })
294
+ })
295
+ }
296
+
297
+ test('root cancellation cannot become successful conversation recovery', async () => {
298
+ await exercise('root-cancel')
299
+ await exercise('observer-root-cancel')
300
+ })
301
+ test('a failed observed invocation settles without waiting for events that cannot arrive', async () => {
302
+ const { output } = await exercise('observer-unavailable')
303
+ expect(output.errors.length).toBeGreaterThan(0)
304
+ expect(output.turns).toHaveLength(0)
305
+ })
@@ -0,0 +1,88 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import {
4
+ canonicalJson,
5
+ decodeJson1,
6
+ JSON_1_LIMITS,
7
+ type JsonValue,
8
+ validateJson1,
9
+ } from '../src/json.js'
10
+
11
+ const bytes = (text: string) => new TextEncoder().encode(text)
12
+
13
+ describe('local complete JSON/0 codec', () => {
14
+ test('decodes and canonicalizes portable scalars, magic names and Unicode', () => {
15
+ const value = decodeJson1(bytes('{"__proto__":1,"z":-0,"a":[true,false,null,"😀",1.25]}'))
16
+ expect(Object.getPrototypeOf(value)).toBeNull()
17
+ expect(new TextDecoder().decode(canonicalJson(value))).toBe(
18
+ '{"__proto__":1,"a":[true,false,null,"😀",1.25],"z":0}',
19
+ )
20
+ expect(decodeJson1(bytes('9007199254740991'))).toBe(Number.MAX_SAFE_INTEGER)
21
+ expect(Object.is(decodeJson1(bytes('-0')), -0)).toBe(true)
22
+ })
23
+
24
+ test('rejects duplicate keys, non-JSON syntax and out-of-domain numbers', () => {
25
+ for (const text of [
26
+ '',
27
+ '{"a":1,"\\u0061":2}',
28
+ '01',
29
+ '+1',
30
+ '.1',
31
+ '1.',
32
+ '1e',
33
+ 'NaN',
34
+ '1e400',
35
+ '9007199254740993',
36
+ '-9007199254740992',
37
+ 'true false',
38
+ '[1,]',
39
+ '{"a":1,}',
40
+ '"\\ud800"',
41
+ '"\\udc00"',
42
+ '\ufeff{}',
43
+ ]) {
44
+ expect(() => decodeJson1(bytes(text))).toThrow()
45
+ }
46
+ expect(() => decodeJson1(Uint8Array.from([0x22, 0xc0, 0xaf, 0x22]))).toThrow('UTF-8')
47
+ })
48
+
49
+ test('rejects encoded document, depth, node, container, key, string and token overflow', () => {
50
+ expect(() => decodeJson1(new Uint8Array(JSON_1_LIMITS.bytes + 1))).toThrow()
51
+ expect(() => decodeJson1(bytes('['.repeat(128) + '0' + ']'.repeat(128)))).toThrow('depth')
52
+ expect(() => validateJson1(Array(65_537).fill(0))).toThrow('array items')
53
+ expect(() => validateJson1(Array.from({ length: 4 }, () => Array(65_536).fill(0)))).toThrow(
54
+ 'nodes',
55
+ )
56
+ expect(() => validateJson1({ ['a'.repeat(1025)]: 0 })).toThrow('string bytes')
57
+ expect(() => validateJson1('a'.repeat(8_388_609))).toThrow('string bytes')
58
+ expect(() => decodeJson1(bytes(`0.${'0'.repeat(128)}1`))).toThrow('number token')
59
+ expect(() => validateJson1(['\0'.repeat(3_000_000)])).toThrow('encoded bytes')
60
+ })
61
+
62
+ test('checks ordinary host data without invoking accessors or serialization hooks', () => {
63
+ let called = false
64
+ const object = {
65
+ get value() {
66
+ called = true
67
+ return 'unsafe'
68
+ },
69
+ }
70
+ expect(() => validateJson1(object)).toThrow()
71
+ expect(called).toBe(false)
72
+ const cycle: JsonValue[] = []
73
+ cycle.push(cycle)
74
+ for (const value of [
75
+ cycle,
76
+ new Date(),
77
+ new Map(),
78
+ undefined,
79
+ Infinity,
80
+ NaN,
81
+ '\udfff',
82
+ new Array(1),
83
+ { [Symbol('hidden')]: 1 },
84
+ ]) {
85
+ expect(() => validateJson1(value)).toThrow()
86
+ }
87
+ })
88
+ })