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

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 +45 -5
  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 +73 -0
  46. package/test/conversation.test.ts +328 -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,328 @@
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
+ if (command.type === 'interrupt' && mode === 'not-running-race') {
140
+ // The turn finishes as the interruption reaches the Agent. The
141
+ // terminal result must remain distinct from the control rejection.
142
+ terminal(command.turn)
143
+ item({
144
+ type: 'rejected',
145
+ command: 'interrupt',
146
+ turn: command.turn,
147
+ code: 'NOT_RUNNING',
148
+ })
149
+ } else {
150
+ item({ type: 'accepted', command: command.type, turn: command.turn })
151
+ if (command.type === 'prompt') {
152
+ if (mode === 'late-error') {
153
+ nativeSettled = true
154
+ fail(call, 'EXECUTION_FAILED')
155
+ } else if (!['interrupt', 'completion-race', 'not-running-race'].includes(mode))
156
+ terminal(command.turn)
157
+ } else if (command.type === 'interrupt')
158
+ terminal(command.turn, mode === 'interrupt' ? 'cancelled' : 'result')
159
+ }
160
+ } else if (request.method === 'channel/close') {
161
+ expect(p.endpoint).toBe('commands:s')
162
+ closed = true
163
+ ok(request, null)
164
+ if (call && !nativeSettled) {
165
+ nativeSettled = true
166
+ if (eventRead) {
167
+ ok(eventRead, { end: { lastSequence: 2 } })
168
+ eventRead = undefined
169
+ }
170
+ if (p.error) fail(call, 'EXECUTION_FAILED')
171
+ else
172
+ ok(call, {
173
+ outcome: 'done',
174
+ output: {
175
+ turns: mode === 'callback-error' ? 1 : 2,
176
+ ...(['retained', 'invalid-session', 'unsolicited-session'].includes(mode)
177
+ ? {
178
+ session: {
179
+ status: 'retained',
180
+ reference:
181
+ mode === 'invalid-session'
182
+ ? 'x'.repeat(36)
183
+ : '013579ab-cdef-4567-89ab-0123456789ab',
184
+ },
185
+ }
186
+ : {}),
187
+ },
188
+ })
189
+ }
190
+ } else if (request.method === 'channel/release') {
191
+ if (p.endpoint === 'events:r') {
192
+ ok(
193
+ request,
194
+ mode === 'observer-disposal'
195
+ ? { status: 'failed', code: 'LAGGED' }
196
+ : { status: 'released' },
197
+ )
198
+ continue
199
+ }
200
+ expect(['replies:r', ...(mode === 'partial-allocation' ? ['commands:r'] : [])]).toContain(
201
+ p.endpoint,
202
+ )
203
+ if (call) expect(nativeSettled).toBe(true)
204
+ if (mode === 'disposal-error') ok(request, { status: 'failed', code: 'LAGGED' })
205
+ else ok(request, { status: 'released' })
206
+ } else if (request.method === 'request/cancel') {
207
+ if (eventRead && eventRead.id === p.requestId) {
208
+ fail(eventRead, 'CANCELLED')
209
+ eventRead = undefined
210
+ } else if (read && read.id === p.requestId) {
211
+ fail(read, 'CANCELLED')
212
+ read = undefined
213
+ } else if (
214
+ (mode === 'root-cancel' || mode === 'observer-root-cancel') &&
215
+ call &&
216
+ call.id === p.requestId
217
+ ) {
218
+ nativeSettled = true
219
+ fail(call, 'CANCELLED')
220
+ } else if (
221
+ operations.some(
222
+ (operation) => operation.id === p.requestId && operation.method === 'channel/next',
223
+ )
224
+ ) {
225
+ // Cancellation may arrive after this fixture already answered the read.
226
+ } else
227
+ throw Error('Helper cancelled its invocation waiter instead of awaiting settlement')
228
+ } else throw Error(`unexpected method ${request.method}`)
229
+ }
230
+ }
231
+ expect(await process.exited, await errors).toBe(0)
232
+ if (mode === 'root-cancel' || mode === 'observer-root-cancel') {
233
+ expect(final?.error?.data?.code).toBe('CANCELLED')
234
+ return { output: null, operations }
235
+ }
236
+ expect(final?.result?.outcome).toBe('done')
237
+ expect(closed).toBe(true)
238
+ return { output: final.result.output, operations }
239
+ } finally {
240
+ clearTimeout(timer)
241
+ process.kill()
242
+ await process.exited
243
+ }
244
+ }
245
+
246
+ for (const mode of ['normal', 'interrupt', 'completion-race', 'not-running-race']) {
247
+ test(`public conversation helper: ${mode}`, async () => {
248
+ const { output, operations } = await exercise(mode)
249
+ expect(output.turns).toHaveLength(2)
250
+ expect(output.value.second.type).toBe(mode === 'interrupt' ? 'cancelled' : 'result')
251
+ if (mode === 'interrupt' || mode === 'completion-race')
252
+ expect(output.value.control).toBe('accepted')
253
+ if (mode === 'not-running-race') {
254
+ expect(output.value.control).toBe('not-running')
255
+ expect(
256
+ operations
257
+ .filter((operation) => operation.method === 'channel/send')
258
+ .map((operation) => operation.params.value.type),
259
+ ).toEqual(['prompt', 'interrupt', 'close'])
260
+ }
261
+ expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 2 } })
262
+ })
263
+ }
264
+ test('helper filters live public events without caller channel coordination', async () => {
265
+ const { output } = await exercise('observer')
266
+ expect(output.displayed).toEqual(['visible draft'])
267
+ expect(output.observation).toEqual({ status: 'complete' })
268
+ expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 2 } })
269
+ })
270
+ for (const mode of ['observer-throws', 'observer-async', 'observer-lagged', 'observer-disposal']) {
271
+ test(`${mode} leaves successful Agent settlement intact with explicit observation loss`, async () => {
272
+ const { output } = await exercise(mode)
273
+ expect(output.observation.status).toBe('incomplete')
274
+ expect(output.observation.errors.length).toBeGreaterThan(0)
275
+ expect(output.turns).toHaveLength(2)
276
+ expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 2 } })
277
+ })
278
+ }
279
+ test('public conversation helper keeps requested retention on final settlement only', async () => {
280
+ const { output } = await exercise('retained')
281
+ expect(output.turns).toHaveLength(2)
282
+ expect(output.turns.every((turn: any) => turn.result.output.session === undefined)).toBe(true)
283
+ expect(output.settlement).toEqual({
284
+ outcome: 'done',
285
+ output: {
286
+ turns: 2,
287
+ session: { status: 'retained', reference: '013579ab-cdef-4567-89ab-0123456789ab' },
288
+ },
289
+ })
290
+ })
291
+ for (const mode of ['missing-session', 'invalid-session', 'unsolicited-session']) {
292
+ test(`public conversation helper refuses ${mode} while retaining settled turns`, async () => {
293
+ const { output } = await exercise(mode)
294
+ expect(output.errors).toContain('Agent omitted matching conversation settlement')
295
+ expect(output.turns).toHaveLength(2)
296
+ expect(output.settlement.output.turns).toBe(2)
297
+ })
298
+ }
299
+
300
+ for (const mode of [
301
+ 'unavailable',
302
+ 'late-error',
303
+ 'malformed',
304
+ 'partial-allocation',
305
+ 'callback-error',
306
+ 'abandoned',
307
+ 'disposal-error',
308
+ ]) {
309
+ test(`public conversation helper retains honest ${mode}`, async () => {
310
+ const { output } = await exercise(mode)
311
+ expect(output.errors.length).toBeGreaterThan(0)
312
+ expect(output.turns).toHaveLength(
313
+ mode === 'disposal-error' ? 2 : ['late-error', 'callback-error'].includes(mode) ? 1 : 0,
314
+ )
315
+ if (mode === 'callback-error')
316
+ expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 1 } })
317
+ })
318
+ }
319
+
320
+ test('root cancellation cannot become successful conversation recovery', async () => {
321
+ await exercise('root-cancel')
322
+ await exercise('observer-root-cancel')
323
+ })
324
+ test('a failed observed invocation settles without waiting for events that cannot arrive', async () => {
325
+ const { output } = await exercise('observer-unavailable')
326
+ expect(output.errors.length).toBeGreaterThan(0)
327
+ expect(output.turns).toHaveLength(0)
328
+ })
@@ -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
+ })