ajp-protocol 0.1.0
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/.github/workflows/publish.yml +20 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/examples/agent-to-agent/example.js +67 -0
- package/examples/human-to-agent/example.js +83 -0
- package/examples/orchestrator/example.js +55 -0
- package/package.json +20 -0
- package/schema/job-offer.json +95 -0
- package/spec/SPEC.md +333 -0
- package/src/client.js +196 -0
- package/src/index.js +24 -0
- package/src/server.js +273 -0
- package/src/utils.js +93 -0
package/spec/SPEC.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Agent Job Protocol (AJP)
|
|
2
|
+
**Version 0.1 — Provenance Protocol Family**
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## What is AJP?
|
|
7
|
+
|
|
8
|
+
AJP is the standard interaction layer for the agent internet.
|
|
9
|
+
|
|
10
|
+
It defines how any party — a human, an agent, or an orchestrator — hands a job
|
|
11
|
+
to another agent, tracks its progress, and receives the result. The envelope is
|
|
12
|
+
always the same. The task inside varies by domain.
|
|
13
|
+
|
|
14
|
+
Think of it as HTTP for agent work. HTTP defines how messages travel across the
|
|
15
|
+
web without caring what the content is. AJP defines how jobs travel between
|
|
16
|
+
agents without caring what the job is.
|
|
17
|
+
|
|
18
|
+
It is intentionally minimal. Three endpoints. Three message types. JSON
|
|
19
|
+
throughout. Runs over standard HTTP. No new infrastructure required.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Design principles
|
|
24
|
+
|
|
25
|
+
**One envelope, any task.** The JobOffer wrapper is universal. The `task` field
|
|
26
|
+
inside is yours to define. An agent that searches the web and an agent that
|
|
27
|
+
processes invoices use the same protocol.
|
|
28
|
+
|
|
29
|
+
**Trust is built in, not bolted on.** Every JobOffer is signed by the sender.
|
|
30
|
+
Every receiver verifies the sender against the Provenance index before accepting.
|
|
31
|
+
Trust verification is part of the protocol, not optional middleware.
|
|
32
|
+
|
|
33
|
+
**Three parties, same protocol.** A human hiring an agent, an agent hiring an
|
|
34
|
+
agent, and an orchestrator delegating to sub-agents all use identical message
|
|
35
|
+
types. The `from` field distinguishes them.
|
|
36
|
+
|
|
37
|
+
**Async by default.** Jobs are accepted and executed asynchronously. The
|
|
38
|
+
`callback` field tells the receiver where to send the result. Polling via
|
|
39
|
+
`GET /jobs/:id` is also supported for simpler implementations.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## The three use cases
|
|
44
|
+
|
|
45
|
+
### 1. Human hiring an agent
|
|
46
|
+
|
|
47
|
+
A human (or a platform acting on their behalf) sends a JobOffer to an agent.
|
|
48
|
+
The `from.type` is `human`. No Provenance verification of the sender is required
|
|
49
|
+
— humans are not indexed agents. Platform-level auth handles human identity.
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Human / Platform ──JobOffer──► Agent
|
|
53
|
+
◄──JobResult──
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Agent hiring an agent
|
|
57
|
+
|
|
58
|
+
An agent sends a JobOffer to another agent. The `from.type` is `agent`. The
|
|
59
|
+
receiving agent MUST verify the sender's Provenance ID before accepting.
|
|
60
|
+
The sender must have `delegate:agents` in its declared capabilities.
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Agent A ──JobOffer──► Agent B
|
|
64
|
+
◄──JobResult──
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Orchestrator delegating to sub-agents
|
|
68
|
+
|
|
69
|
+
An orchestrator (itself an agent with a Provenance ID) breaks a task into
|
|
70
|
+
subtasks and delegates each to a specialist agent. The full chain is auditable —
|
|
71
|
+
every job references its `parent_job_id`, allowing reconstruction of the
|
|
72
|
+
complete execution tree.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Orchestrator ──JobOffer──► Sub-agent A
|
|
76
|
+
──JobOffer──► Sub-agent B
|
|
77
|
+
◄──JobResult── Sub-agent A
|
|
78
|
+
◄──JobResult── Sub-agent B
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Endpoints
|
|
84
|
+
|
|
85
|
+
Every AJP-compliant agent exposes three endpoints:
|
|
86
|
+
|
|
87
|
+
### POST /jobs
|
|
88
|
+
Receive a new job offer.
|
|
89
|
+
|
|
90
|
+
**Request body:** `JobOffer`
|
|
91
|
+
**Response 202:** `{ job_id, status: "accepted" }`
|
|
92
|
+
**Response 400:** `{ error, reason }` — malformed offer
|
|
93
|
+
**Response 403:** `{ error, reason }` — trust check failed
|
|
94
|
+
**Response 402:** `{ error, reason }` — budget insufficient
|
|
95
|
+
**Response 429:** `{ error, retry_after }` — agent busy
|
|
96
|
+
|
|
97
|
+
### GET /jobs/:job_id
|
|
98
|
+
Check the status of a job.
|
|
99
|
+
|
|
100
|
+
**Response 200:** `JobStatus`
|
|
101
|
+
**Response 404:** job not found
|
|
102
|
+
|
|
103
|
+
### POST /jobs/:job_id/ack
|
|
104
|
+
Confirm result received. Triggers payment settlement if applicable.
|
|
105
|
+
|
|
106
|
+
**Request body:** `{ received: true, feedback?: string }`
|
|
107
|
+
**Response 200:** `{ settled: true }`
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Message types
|
|
112
|
+
|
|
113
|
+
### JobOffer
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"ajp": "0.1",
|
|
118
|
+
"job_id": "job_01J8X2K9M3N4P5Q6R7S8T9U0V1",
|
|
119
|
+
"parent_job_id": null,
|
|
120
|
+
|
|
121
|
+
"from": {
|
|
122
|
+
"type": "human",
|
|
123
|
+
"id": "user_abc123",
|
|
124
|
+
"provenance_id": null
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
"to": {
|
|
128
|
+
"provenance_id": "provenance:github:alice/research-assistant"
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
"task": {
|
|
132
|
+
"type": "research",
|
|
133
|
+
"instruction": "Find the three most cited papers on transformer attention mechanisms published in 2024. Return titles, authors, citation counts, and a 2-sentence summary of each.",
|
|
134
|
+
"input": {},
|
|
135
|
+
"output_format": "json"
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
"context": {
|
|
139
|
+
"credentials": {},
|
|
140
|
+
"memory": [],
|
|
141
|
+
"constraints": []
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
"budget": {
|
|
145
|
+
"max_usd": 0.50,
|
|
146
|
+
"max_seconds": 120,
|
|
147
|
+
"max_llm_tokens": 10000
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
"callback": {
|
|
151
|
+
"url": "https://clawmarket.com/api/jobs/job_01J8X2K9M3N4P5Q6R7S8T9U0V1/result",
|
|
152
|
+
"headers": { "Authorization": "Bearer token_xyz" }
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
"issued_at": "2026-03-05T10:00:00Z",
|
|
156
|
+
"expires_at": "2026-03-05T10:02:00Z",
|
|
157
|
+
|
|
158
|
+
"signature": "sha256:a1b2c3d4..."
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### JobStatus
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"ajp": "0.1",
|
|
167
|
+
"job_id": "job_01J8X2K9M3N4P5Q6R7S8T9U0V1",
|
|
168
|
+
"status": "running",
|
|
169
|
+
"progress": 0.4,
|
|
170
|
+
"message": "Found 2 of 3 papers, searching for third",
|
|
171
|
+
"started_at": "2026-03-05T10:00:01Z",
|
|
172
|
+
"updated_at": "2026-03-05T10:00:08Z",
|
|
173
|
+
"estimated_completion": "2026-03-05T10:00:20Z"
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Status values: `accepted` `running` `completed` `failed` `rejected` `expired`
|
|
178
|
+
|
|
179
|
+
### JobResult
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"ajp": "0.1",
|
|
184
|
+
"job_id": "job_01J8X2K9M3N4P5Q6R7S8T9U0V1",
|
|
185
|
+
"status": "completed",
|
|
186
|
+
|
|
187
|
+
"output": {
|
|
188
|
+
"papers": [
|
|
189
|
+
{
|
|
190
|
+
"title": "Flash Attention 3",
|
|
191
|
+
"authors": ["Tri Dao", "Daniel Y. Fu"],
|
|
192
|
+
"citations": 412,
|
|
193
|
+
"summary": "..."
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
"usage": {
|
|
199
|
+
"llm_tokens": 4821,
|
|
200
|
+
"duration_seconds": 18,
|
|
201
|
+
"cost_usd": 0.12
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
"agent": {
|
|
205
|
+
"provenance_id": "provenance:github:alice/research-assistant",
|
|
206
|
+
"version": "1.2.0",
|
|
207
|
+
"model": { "provider": "anthropic", "model_id": "claude-sonnet-4-5" }
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
"completed_at": "2026-03-05T10:00:19Z",
|
|
211
|
+
"signature": "sha256:e5f6g7h8..."
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Trust verification
|
|
218
|
+
|
|
219
|
+
When `from.type` is `agent`, the receiving agent MUST run a trust check before
|
|
220
|
+
accepting the job. Using the `provenance-protocol` SDK:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
import { provenance } from 'provenance-protocol';
|
|
224
|
+
|
|
225
|
+
const result = await provenance.gate(offer.from.provenance_id, {
|
|
226
|
+
requireDeclared: true,
|
|
227
|
+
requireConstraints: [], // add what your agent requires
|
|
228
|
+
requireClean: true,
|
|
229
|
+
requireMinAge: 7, // don't accept jobs from brand-new agents
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!result.allowed) {
|
|
233
|
+
return res.status(403).json({ error: 'Trust check failed', reason: result.reason });
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
For `from.type === 'human'`, trust verification is handled by the platform
|
|
238
|
+
(ClawMarket, SkillsMP, etc.) before the JobOffer is issued.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Signature
|
|
243
|
+
|
|
244
|
+
Every JobOffer and JobResult is signed by the sender. The signature covers the
|
|
245
|
+
full message body excluding the `signature` field itself.
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
signature = "sha256:" + hex(HMAC-SHA256(JSON.stringify(body_without_signature), sender_secret))
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Receiving agents verify signatures before processing. The `ajp-protocol` SDK
|
|
252
|
+
handles signing and verification automatically.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Adding AJP to your agent
|
|
257
|
+
|
|
258
|
+
### Expose the three endpoints
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
import { AJPServer } from 'ajp-protocol';
|
|
262
|
+
|
|
263
|
+
const server = new AJPServer({
|
|
264
|
+
provenanceId: 'provenance:github:alice/research-assistant',
|
|
265
|
+
secret: process.env.AJP_SECRET,
|
|
266
|
+
onJob: async (job) => {
|
|
267
|
+
// your agent logic here
|
|
268
|
+
return { papers: [...] };
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Express / Next.js / any HTTP framework
|
|
273
|
+
app.post('/jobs', server.receive());
|
|
274
|
+
app.get('/jobs/:id', server.status());
|
|
275
|
+
app.post('/jobs/:id/ack', server.ack());
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Send a job to another agent
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
import { AJPClient } from 'ajp-protocol';
|
|
282
|
+
|
|
283
|
+
const client = new AJPClient({
|
|
284
|
+
from: { type: 'agent', provenance_id: 'provenance:github:alice/orchestrator' },
|
|
285
|
+
secret: process.env.AJP_SECRET,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const result = await client.send(
|
|
289
|
+
'provenance:github:bob/pdf-extractor',
|
|
290
|
+
{
|
|
291
|
+
type: 'extract',
|
|
292
|
+
instruction: 'Extract all tables from this PDF',
|
|
293
|
+
input: { url: 'https://example.com/report.pdf' },
|
|
294
|
+
},
|
|
295
|
+
{ max_usd: 0.25, max_seconds: 60 }
|
|
296
|
+
);
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## PROVENANCE.yml integration
|
|
302
|
+
|
|
303
|
+
Agents that implement AJP should declare it:
|
|
304
|
+
|
|
305
|
+
```yaml
|
|
306
|
+
provenance: "0.1"
|
|
307
|
+
name: "Research Assistant"
|
|
308
|
+
|
|
309
|
+
capabilities:
|
|
310
|
+
- read:web
|
|
311
|
+
- ajp:receiver # this agent accepts AJP jobs
|
|
312
|
+
- ajp:sender # this agent can send AJP jobs to others
|
|
313
|
+
|
|
314
|
+
ajp:
|
|
315
|
+
endpoint: "https://alice.dev/api/agent/jobs"
|
|
316
|
+
version: "0.1"
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The Provenance crawler reads the `ajp.endpoint` field and indexes it. Senders
|
|
320
|
+
can discover an agent's AJP endpoint without out-of-band communication.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Versioning
|
|
325
|
+
|
|
326
|
+
The `ajp` field in every message declares the spec version. `0.1` is the current
|
|
327
|
+
version. Future versions add fields, never remove them.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
*AJP v0.1 — Provenance Protocol Family — MIT License*
|
|
332
|
+
*https://provenance.dev/ajp*
|
|
333
|
+
*https://github.com/provenance-protocol/ajp*
|
package/src/client.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AJPClient — Send jobs to other agents.
|
|
3
|
+
*
|
|
4
|
+
* Used by: humans (via platforms), agents, orchestrators.
|
|
5
|
+
* Consistent with provenance-protocol SDK class structure.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { sign, generateJobId, validateOffer } from './utils.js';
|
|
9
|
+
import { Provenance } from 'provenance-protocol';
|
|
10
|
+
|
|
11
|
+
export class AJPClient {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} opts
|
|
15
|
+
* @param {object} opts.from — sender identity
|
|
16
|
+
* @param {string} opts.from.type — 'human' | 'agent' | 'orchestrator'
|
|
17
|
+
* @param {string} [opts.from.id] — platform user ID (human only)
|
|
18
|
+
* @param {string} [opts.from.provenance_id] — required for agent/orchestrator
|
|
19
|
+
* @param {string} opts.secret — HMAC signing secret
|
|
20
|
+
* @param {string} [opts.provenanceApiUrl] — override Provenance API URL
|
|
21
|
+
* @param {number} [opts.defaultTimeoutMs] — default job timeout in ms (30s)
|
|
22
|
+
*/
|
|
23
|
+
constructor({ from, secret, provenanceApiUrl, defaultTimeoutMs = 30000 }) {
|
|
24
|
+
this.from = from;
|
|
25
|
+
this.secret = secret;
|
|
26
|
+
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
27
|
+
this.provenance = new Provenance({ apiUrl: provenanceApiUrl });
|
|
28
|
+
|
|
29
|
+
if ((from.type === 'agent' || from.type === 'orchestrator') && !from.provenance_id) {
|
|
30
|
+
throw new Error('from.provenance_id required when type is agent or orchestrator');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Main send method ──────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Send a job to an agent and wait for the result.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} toProvenanceId — receiving agent's Provenance ID
|
|
40
|
+
* @param {object} task — { type, instruction, input?, output_format? }
|
|
41
|
+
* @param {object} [budget] — { max_usd, max_seconds?, max_llm_tokens? }
|
|
42
|
+
* @param {object} [opts]
|
|
43
|
+
* @param {string} [opts.parentJobId] — set for sub-tasks in orchestration
|
|
44
|
+
* @param {object} [opts.context] — { credentials?, memory?, constraints? }
|
|
45
|
+
* @param {object} [opts.callback] — { url, headers? } for async delivery
|
|
46
|
+
* @param {number} [opts.pollIntervalMs] — how often to poll for result (2000)
|
|
47
|
+
* @returns {Promise<JobResult>}
|
|
48
|
+
*/
|
|
49
|
+
async send(toProvenanceId, task, budget = {}, opts = {}) {
|
|
50
|
+
// Resolve the agent's AJP endpoint from Provenance
|
|
51
|
+
const endpoint = await this._resolveEndpoint(toProvenanceId);
|
|
52
|
+
|
|
53
|
+
// Build the job offer
|
|
54
|
+
const jobId = generateJobId();
|
|
55
|
+
const now = new Date();
|
|
56
|
+
const expiresAt = new Date(now.getTime() + (budget.max_seconds || 120) * 1000);
|
|
57
|
+
|
|
58
|
+
const offer = {
|
|
59
|
+
ajp: '0.1',
|
|
60
|
+
job_id: jobId,
|
|
61
|
+
parent_job_id: opts.parentJobId || null,
|
|
62
|
+
|
|
63
|
+
from: {
|
|
64
|
+
type: this.from.type,
|
|
65
|
+
id: this.from.id || null,
|
|
66
|
+
provenance_id: this.from.provenance_id || null,
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
to: { provenance_id: toProvenanceId },
|
|
70
|
+
|
|
71
|
+
task: {
|
|
72
|
+
type: task.type,
|
|
73
|
+
instruction: task.instruction,
|
|
74
|
+
input: task.input || {},
|
|
75
|
+
output_format: task.output_format || 'json',
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
context: {
|
|
79
|
+
credentials: opts.context?.credentials || {},
|
|
80
|
+
memory: opts.context?.memory || [],
|
|
81
|
+
constraints: opts.context?.constraints || [],
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
budget: {
|
|
85
|
+
max_usd: budget.max_usd ?? 1.0,
|
|
86
|
+
max_seconds: budget.max_seconds ?? 120,
|
|
87
|
+
max_llm_tokens: budget.max_llm_tokens ?? 10000,
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
callback: opts.callback || null,
|
|
91
|
+
issued_at: now.toISOString(),
|
|
92
|
+
expires_at: expiresAt.toISOString(),
|
|
93
|
+
signature: '',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Sign it
|
|
97
|
+
offer.signature = sign(offer, this.secret);
|
|
98
|
+
|
|
99
|
+
// Validate before sending
|
|
100
|
+
const { valid, errors } = validateOffer(offer);
|
|
101
|
+
if (!valid) throw new Error(`Invalid JobOffer: ${errors.join(', ')}`);
|
|
102
|
+
|
|
103
|
+
// Send
|
|
104
|
+
const res = await fetch(`${endpoint}/jobs`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify(offer),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (res.status === 403) {
|
|
111
|
+
const body = await res.json();
|
|
112
|
+
throw new Error(`Agent rejected job (trust check failed): ${body.reason}`);
|
|
113
|
+
}
|
|
114
|
+
if (res.status === 402) {
|
|
115
|
+
const body = await res.json();
|
|
116
|
+
throw new Error(`Agent rejected job (budget insufficient): ${body.reason}`);
|
|
117
|
+
}
|
|
118
|
+
if (res.status === 429) {
|
|
119
|
+
const body = await res.json();
|
|
120
|
+
throw new Error(`Agent busy. Retry after ${body.retry_after}s`);
|
|
121
|
+
}
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
const body = await res.json().catch(() => ({}));
|
|
124
|
+
throw new Error(`Agent returned ${res.status}: ${body.error || 'unknown error'}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const accepted = await res.json();
|
|
128
|
+
|
|
129
|
+
// If async (callback provided), return the acceptance immediately
|
|
130
|
+
if (opts.callback) return { job_id: accepted.job_id, status: 'accepted' };
|
|
131
|
+
|
|
132
|
+
// Otherwise poll for result
|
|
133
|
+
return this._poll(endpoint, accepted.job_id, {
|
|
134
|
+
pollIntervalMs: opts.pollIntervalMs || 2000,
|
|
135
|
+
timeoutMs: this.defaultTimeoutMs,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Polling ──────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
async _poll(endpoint, jobId, { pollIntervalMs, timeoutMs }) {
|
|
142
|
+
const deadline = Date.now() + timeoutMs;
|
|
143
|
+
|
|
144
|
+
while (Date.now() < deadline) {
|
|
145
|
+
await this._sleep(pollIntervalMs);
|
|
146
|
+
|
|
147
|
+
const res = await fetch(`${endpoint}/jobs/${jobId}`);
|
|
148
|
+
if (!res.ok) throw new Error(`Status check failed: ${res.status}`);
|
|
149
|
+
|
|
150
|
+
const status = await res.json();
|
|
151
|
+
|
|
152
|
+
if (status.status === 'completed') {
|
|
153
|
+
// Acknowledge receipt
|
|
154
|
+
await fetch(`${endpoint}/jobs/${jobId}/ack`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify({ received: true }),
|
|
158
|
+
}).catch(() => {});
|
|
159
|
+
return status;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (status.status === 'failed') {
|
|
163
|
+
throw new Error(`Job failed: ${status.message || 'unknown reason'}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (status.status === 'rejected') {
|
|
167
|
+
throw new Error(`Job rejected: ${status.message || 'unknown reason'}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw new Error(`Job timed out after ${timeoutMs}ms`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Endpoint resolution ───────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
async _resolveEndpoint(provenanceId) {
|
|
177
|
+
try {
|
|
178
|
+
const profile = await this.provenance.check(provenanceId);
|
|
179
|
+
if (!profile.found) throw new Error(`Agent not found in Provenance index: ${provenanceId}`);
|
|
180
|
+
|
|
181
|
+
// AJP endpoint is stored in the agent's PROVENANCE.yml
|
|
182
|
+
const endpoint = profile.provenance_yml?.ajp?.endpoint;
|
|
183
|
+
if (endpoint) return endpoint.replace(/\/$/, '');
|
|
184
|
+
|
|
185
|
+
// Fallback: derive from agent URL
|
|
186
|
+
if (profile.url) return `${profile.url.replace(/\/$/, '')}/api/agent`;
|
|
187
|
+
|
|
188
|
+
throw new Error(`No AJP endpoint found for ${provenanceId}`);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (e.message.includes('No AJP endpoint')) throw e;
|
|
191
|
+
throw new Error(`Could not resolve endpoint for ${provenanceId}: ${e.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
196
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ajp-protocol
|
|
3
|
+
*
|
|
4
|
+
* The Agent Job Protocol — standard interaction layer for the agent internet.
|
|
5
|
+
* Part of the Provenance Protocol family.
|
|
6
|
+
*
|
|
7
|
+
* npm install ajp-protocol
|
|
8
|
+
*
|
|
9
|
+
* Usage (receiving agent):
|
|
10
|
+
* import { AJPServer } from 'ajp-protocol';
|
|
11
|
+
* const server = new AJPServer({ provenanceId, secret, onJob });
|
|
12
|
+
* app.post('/jobs', server.receive());
|
|
13
|
+
* app.get('/jobs/:id', server.status());
|
|
14
|
+
* app.post('/jobs/:id/ack', server.ack());
|
|
15
|
+
*
|
|
16
|
+
* Usage (sending agent or platform):
|
|
17
|
+
* import { AJPClient } from 'ajp-protocol';
|
|
18
|
+
* const client = new AJPClient({ from, secret });
|
|
19
|
+
* const result = await client.send(toProvenanceId, task, budget);
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export { AJPClient } from './client.js';
|
|
23
|
+
export { AJPServer } from './server.js';
|
|
24
|
+
export { sign, verify, generateJobId, validateOffer, JOB_STATUS, FROM_TYPE } from './utils.js';
|