@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,252 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { pathToFileURL } from 'node:url'
6
+ import type { FlowCall, RunContext } from '@jigging/flow'
7
+
8
+ import { agentFlow } from '../src/flow.js'
9
+ import { readPackageSkills } from '../src/skills.js'
10
+
11
+ const temporary: string[] = []
12
+ afterEach(async () => {
13
+ for (const root of temporary.splice(0)) await rm(root, { recursive: true, force: true })
14
+ })
15
+ const fixture = async () => {
16
+ const root = await mkdtemp(join(tmpdir(), 'agent-method-test-'))
17
+ temporary.push(root)
18
+ await mkdir(join(root, 'skills', 'check'), { recursive: true })
19
+ await writeFile(join(root, 'skills', 'check', 'SKILL.md'), 'Exact guidance.\n')
20
+ return { root, url: pathToFileURL(`${root}/`) }
21
+ }
22
+
23
+ describe('explicit package Skill reader', () => {
24
+ test('reads exact UTF-8 files from a relocated package root', async () => {
25
+ const { root, url } = await fixture()
26
+ await mkdir(join(root, 'skills', 'check', 'nested'))
27
+ await writeFile(join(root, 'skills', 'check', 'nested', 'note.txt'), 'café\r\n')
28
+ const result = await readPackageSkills(url, ['check'])
29
+ expect(result).toEqual([
30
+ {
31
+ name: 'check',
32
+ files: [
33
+ { path: 'SKILL.md', text: 'Exact guidance.\n' },
34
+ { path: 'nested/note.txt', text: 'café\r\n' },
35
+ ],
36
+ },
37
+ ])
38
+ expect(Object.isFrozen(result[0]?.files)).toBe(true)
39
+ expect(await readPackageSkills(url, [])).toEqual([])
40
+ })
41
+
42
+ test('rejects duplicate and escaped names, invalid UTF-8, missing files and symlinks', async () => {
43
+ const { root, url } = await fixture()
44
+ for (const names of [['check', 'check'], ['../check'], ['missing']])
45
+ await expect(readPackageSkills(url, names)).rejects.toThrow()
46
+ await writeFile(join(root, 'skills', 'check', 'bad.txt'), Uint8Array.from([0xff]))
47
+ await expect(readPackageSkills(url, ['check'])).rejects.toThrow('UTF-8')
48
+ await rm(join(root, 'skills', 'check', 'bad.txt'))
49
+ await symlink(
50
+ join(root, 'skills', 'check', 'SKILL.md'),
51
+ join(root, 'skills', 'check', 'linked'),
52
+ )
53
+ await expect(readPackageSkills(url, ['check'])).rejects.toThrow('symlink')
54
+ await rm(join(root, 'skills', 'check', 'linked'))
55
+ await symlink(root, join(root, 'alias'))
56
+ await expect(readPackageSkills(pathToFileURL(`${root}/alias/`), ['check'])).rejects.toThrow(
57
+ 'symlink',
58
+ )
59
+ })
60
+
61
+ test('bounds file bytes before reading them', async () => {
62
+ const { root, url } = await fixture()
63
+ await writeFile(join(root, 'skills', 'check', 'large'), new Uint8Array(1_048_577))
64
+ await expect(readPackageSkills(url, ['check'])).rejects.toThrow('1 MiB')
65
+ })
66
+ })
67
+
68
+ describe('ordinary Flow wiring', () => {
69
+ test('rejects missing model settings and unsupported channels before dispatch', async () => {
70
+ let calls = 0
71
+ const run = {
72
+ input: { instructions: 'Answer.' },
73
+ settings: {},
74
+ attachments: {},
75
+ channels: {},
76
+ signal: new AbortController().signal,
77
+ call: async () => {
78
+ calls++
79
+ throw new Error('must not dispatch')
80
+ },
81
+ } as unknown as RunContext
82
+ await expect(agentFlow(run)).rejects.toThrow('model')
83
+ await expect(
84
+ agentFlow({ ...run, input: { instructions: 'Answer.', conversation: true } } as RunContext),
85
+ ).rejects.toMatchObject({ code: 'UNAVAILABLE' })
86
+ for (const session of [
87
+ { retain: true },
88
+ { restore: '013579ab-cdef-4567-89ab-0123456789ab' },
89
+ null,
90
+ ])
91
+ await expect(
92
+ agentFlow({ ...run, input: { instructions: 'Answer.', session } } as RunContext),
93
+ ).rejects.toMatchObject({ code: 'UNAVAILABLE' })
94
+ await expect(
95
+ agentFlow({
96
+ ...run,
97
+ settings: { model: 'x' },
98
+ channels: { events: {} },
99
+ } as unknown as RunContext),
100
+ ).rejects.toThrow('channels')
101
+ expect(calls).toBe(0)
102
+ await expect(
103
+ agentFlow({ ...run, input: { instructions: 'Work.', skills: null } } as RunContext),
104
+ ).rejects.toThrow()
105
+ expect(calls).toBe(0)
106
+ })
107
+ test('prepares once and requests one HTTP completion with the original cancellation signal', async () => {
108
+ const { url } = await fixture()
109
+ let calls = 0
110
+ const signal = new AbortController().signal
111
+ const run = {
112
+ input: { instructions: 'Answer.', skills: await readPackageSkills(url, ['check']) },
113
+ settings: { model: 'fixture-model' },
114
+ attachments: {},
115
+ channels: {},
116
+ signal,
117
+ call: async (call: FlowCall, options: { signal: AbortSignal }) => {
118
+ calls += 1
119
+ expect(call.operationId).toBe('completion')
120
+ expect(call.slot).toBe('http')
121
+ expect(call.channels).toBeUndefined()
122
+ expect((call.input as any).response).toBe('json')
123
+ expect(options.signal).toBe(signal)
124
+ const body = (call.input as any).body
125
+ expect(body).toMatchObject({
126
+ model: 'fixture-model',
127
+ max_completion_tokens: 4096,
128
+ stream: false,
129
+ store: false,
130
+ n: 1,
131
+ })
132
+ expect(body.messages[0].content).toContain('Exact guidance.')
133
+ return {
134
+ outcome: 'done',
135
+ output: {
136
+ status: 200,
137
+ body: {
138
+ object: 'chat.completion',
139
+ choices: [
140
+ {
141
+ index: 0,
142
+ finish_reason: 'stop',
143
+ message: { role: 'assistant', content: 'Answer.' },
144
+ },
145
+ ],
146
+ },
147
+ },
148
+ }
149
+ },
150
+ } as unknown as RunContext
151
+ expect(await agentFlow(run)).toEqual({ outcome: 'done', output: { text: 'Answer.' } })
152
+ expect(calls).toBe(1)
153
+ })
154
+
155
+ test('keeps operational failure as failure without a retry', async () => {
156
+ let calls = 0
157
+ const failure = new Error('uncertain transport')
158
+ const run = {
159
+ input: { instructions: 'Answer.' },
160
+ settings: { model: 'fixture-model' },
161
+ attachments: {},
162
+ channels: {},
163
+ signal: new AbortController().signal,
164
+ call: async () => {
165
+ calls += 1
166
+ throw failure
167
+ },
168
+ } as unknown as RunContext
169
+ await expect(agentFlow(run)).rejects.toBe(failure)
170
+ expect(calls).toBe(1)
171
+ })
172
+
173
+ test('selected Responses schema mode reaches HTTP once and returns a checked result', async () => {
174
+ let calls = 0
175
+ const signal = new AbortController().signal
176
+ const responseSchema = {
177
+ $schema: 'https://flow.jig.md/schemas/schema-0.json',
178
+ type: 'object',
179
+ properties: { answer: { type: 'string' } },
180
+ required: ['answer'],
181
+ additionalProperties: false,
182
+ }
183
+ const run = {
184
+ input: { instructions: 'Answer.', responseSchema },
185
+ settings: { model: 'fixture-model', api: 'responses', structuredOutput: 'json-schema' },
186
+ attachments: {},
187
+ channels: {},
188
+ signal,
189
+ call: async (call: FlowCall, options: { signal: AbortSignal }) => {
190
+ calls++
191
+ expect(options.signal).toBe(signal)
192
+ expect(call.slot).toBe('http')
193
+ const body = (call.input as any).body
194
+ expect(body.max_output_tokens).toBe(4096)
195
+ expect(body.text.format.strict).toBe(true)
196
+ expect(body.text.format.schema.$schema).toBeUndefined()
197
+ expect(body.messages).toBeUndefined()
198
+ return {
199
+ outcome: 'done',
200
+ output: {
201
+ status: 200,
202
+ body: {
203
+ object: 'response',
204
+ status: 'completed',
205
+ output: [
206
+ {
207
+ type: 'message',
208
+ role: 'assistant',
209
+ status: 'completed',
210
+ content: [{ type: 'output_text', text: '{"answer":"checked"}' }],
211
+ },
212
+ ],
213
+ },
214
+ },
215
+ }
216
+ },
217
+ } as unknown as RunContext
218
+ expect(await agentFlow(run)).toEqual({
219
+ outcome: 'done',
220
+ output: { text: '{"answer":"checked"}', structured: { answer: 'checked' } },
221
+ })
222
+ expect(calls).toBe(1)
223
+ })
224
+
225
+ test('provider rejection of a strict schema never falls back or retries', async () => {
226
+ for (const api of ['chat-completions', 'responses']) {
227
+ let calls = 0
228
+ const run = {
229
+ input: {
230
+ instructions: 'Answer.',
231
+ responseSchema: {
232
+ $schema: 'https://flow.jig.md/schemas/schema-0.json',
233
+ type: 'object',
234
+ properties: { answer: { type: 'string' } },
235
+ required: ['answer'],
236
+ additionalProperties: false,
237
+ },
238
+ },
239
+ settings: { model: 'fixture-model', api, structuredOutput: 'json-schema' },
240
+ attachments: {},
241
+ channels: {},
242
+ signal: new AbortController().signal,
243
+ call: async () => {
244
+ calls++
245
+ return { outcome: 'done', output: { status: 400, body: 'private-provider-error' } }
246
+ },
247
+ } as unknown as RunContext
248
+ await expect(agentFlow(run)).rejects.toThrow('HTTP 400')
249
+ expect(calls).toBe(1)
250
+ }
251
+ })
252
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "declaration": true,
12
+ "outDir": "dist",
13
+ "rootDir": "src"
14
+ },
15
+ "include": ["src/**/*.ts"],
16
+ "exclude": ["dist", "test"]
17
+ }