@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.
- package/AGENTS.md +98 -0
- package/FLOW.contract.json +241 -0
- package/FLOW.meta.json +8 -0
- package/FLOW.ts +3 -0
- package/LICENSE +373 -0
- package/README.md +369 -0
- package/THIRD_PARTY_NOTICES +9 -0
- package/contracts/acp-public-updates.json +75 -0
- package/contracts/agent-commands.json +88 -0
- package/contracts/agent-replies.json +202 -0
- package/contracts/http-request/contract.json +37 -0
- package/dist/api.d.ts +11 -0
- package/dist/api.js +164 -0
- package/dist/conversation.d.ts +68 -0
- package/dist/conversation.js +346 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +8 -0
- package/dist/flow.d.ts +3 -0
- package/dist/flow.js +2784 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +220 -0
- package/dist/json.d.ts +21 -0
- package/dist/json.js +409 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +180 -0
- package/dist/skills.d.ts +3 -0
- package/dist/skills.js +132 -0
- package/dist/values.d.ts +11 -0
- package/dist/values.js +65 -0
- package/justfile +32 -0
- package/licenses/flow.LICENSE +202 -0
- package/package.json +44 -4
- package/settings.schema.json +12 -0
- package/skills/answer-check/SKILL.md +5 -0
- package/src/api.ts +191 -0
- package/src/conversation.ts +387 -0
- package/src/errors.ts +11 -0
- package/src/flow.ts +66 -0
- package/src/index.ts +325 -0
- package/src/json.ts +406 -0
- package/src/schema.ts +230 -0
- package/src/skills.ts +138 -0
- package/src/values.ts +77 -0
- package/test/api.test.ts +326 -0
- package/test/conversation-fixture.ts +70 -0
- package/test/conversation.test.ts +305 -0
- package/test/json.test.ts +88 -0
- package/test/method.test.ts +308 -0
- package/test/pack.test.ts +81 -0
- package/test/result.test.ts +103 -0
- package/test/skills-flow.test.ts +252 -0
- 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
|
+
}
|