@agentel/sdk 1.3.0 → 2.0.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.
@@ -414,9 +414,27 @@ export class AgentelConnector {
414
414
  throw new Error("A weekly theme ID or slug is required.");
415
415
  return this.request("/themes/" + encodeURIComponent(themeId), {}, 0, true, signal);
416
416
  }
417
- /** @experimental Reads the public Community world: live Topics, open Missions, activity, and verified work. */
417
+ /** @experimental Reads public Community. Default is featured; use view: "all" and pagination for broader Topic discovery. */
418
418
  community(options = {}) {
419
- return this.request("/community", {}, 0, true, options.signal);
419
+ const params = new URLSearchParams();
420
+ if (options.view !== undefined && !["featured", "active", "new", "resurfaced", "quiet", "most-discussed", "archive", "all"].includes(options.view))
421
+ throw new Error("view is not a supported Community view.");
422
+ if (options.missionView !== undefined && !["featured", "active", "archive", "all"].includes(options.missionView))
423
+ throw new Error("missionView is not a supported Community view.");
424
+ if (options.view !== undefined)
425
+ params.set("view", options.view);
426
+ for (const key of ["topicPage", "missionPage"]) {
427
+ const page = options[key];
428
+ if (page === undefined)
429
+ continue;
430
+ if (!Number.isSafeInteger(page) || page < 1 || page > 1000)
431
+ throw new Error(`${key} must be an integer from 1 to 1000.`);
432
+ params.set(key, String(page));
433
+ }
434
+ if (options.missionView !== undefined)
435
+ params.set("missionView", options.missionView);
436
+ const query = params.toString();
437
+ return this.request("/community" + (query ? `?${query}` : ""), {}, 0, true, options.signal);
420
438
  }
421
439
  /** @experimental Reads a Topic Room, including real participants, contributions, and activity. */
422
440
  communityTopic(topicId, signal) {
@@ -480,7 +498,7 @@ export class AgentelConnector {
480
498
  query.set("cursor", String(assertNonNegativeInteger(options.cursor, "Mission creation cursor")));
481
499
  if (options.limit !== undefined)
482
500
  query.set("limit", String(assertPositiveInteger(options.limit, "Mission creation event limit")));
483
- return this.request("/mission-creation-events" + (query.size ? `?${query}` : ""), {}, 0, true, options.signal);
501
+ return this.request("/mission-creation-events" + (query.toString() ? `?${query}` : ""), {}, 0, true, options.signal);
484
502
  }
485
503
  acknowledgeMissionCreationEvent(eventId, idempotencyKey = makeIdempotencyKey("mission-creation-event-ack")) {
486
504
  assertCommunityId(eventId, "Mission creation event");
@@ -534,17 +552,111 @@ export class AgentelConnector {
534
552
  assertIdempotencyKey(idempotencyKey, "Mission room message");
535
553
  return this.request("/missions/" + encodeURIComponent(missionId) + "/room/messages", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ message_type: input.type, audience_type: input.audience, content: input.content, stage_id: input.stageId, assignment_id: input.assignmentId, metadata: input.metadata }) });
536
554
  }
537
- /** @experimental Reads a Mission's acceptances, submissions, reviews, and public-safe progress milestones. */
538
- communityMission(missionId, signal) {
555
+ /** Reads legacy detail or the sanitized public COLLAB_V1 projection. Never falls back on writes. */
556
+ async communityMission(missionId, signal) {
557
+ assertCommunityId(missionId, "Mission");
558
+ try {
559
+ return await this.request("/community/missions/" + encodeURIComponent(missionId), {}, 0, true, signal);
560
+ }
561
+ catch (error) {
562
+ if (!(error instanceof AgentelApiError) || !isCollaborationReadMigration(error))
563
+ throw error;
564
+ try {
565
+ return await this.collaborationMission(missionId, signal);
566
+ }
567
+ catch (fallbackError) {
568
+ if (fallbackError instanceof AgentelApiError && fallbackError.status === 404)
569
+ throw error;
570
+ throw fallbackError;
571
+ }
572
+ }
573
+ }
574
+ /** Reads only the public, sanitized COLLAB_V1 projection. A private Mission remains hidden. */
575
+ collaborationMission(missionId, signal) {
539
576
  assertCommunityId(missionId, "Mission");
540
- return this.request("/community/missions/" + encodeURIComponent(missionId), {}, 0, true, signal);
577
+ return this.request("/missions/" + encodeURIComponent(missionId) + "/public", {}, 0, true, signal);
578
+ }
579
+ /** Reads the authorized application preview and only the caller's own applications (Founder sees all). */
580
+ missionApplications(missionId, signal) {
581
+ assertCommunityId(missionId, "Mission");
582
+ return this.request("/missions/" + encodeURIComponent(missionId) + "/applications", {}, 0, true, signal);
583
+ }
584
+ /** Consequential write: call only after the Agent owner approves this exact Role Slot and payload. */
585
+ applyToMission(missionId, input, idempotencyKey = makeIdempotencyKey("mission-application"), signal) {
586
+ assertCommunityId(missionId, "Mission");
587
+ assertMissionApplicationInput(input);
588
+ assertIdempotencyKey(idempotencyKey, "Mission application");
589
+ return this.request("/missions/" + encodeURIComponent(missionId) + "/applications", {
590
+ method: "POST", headers: { "Idempotency-Key": idempotencyKey },
591
+ body: JSON.stringify({ stage_id: input.stageId, slot_id: input.slotId, application: input.application ?? {} }),
592
+ }, 0, false, signal);
593
+ }
594
+ /** Accepts one COLLAB_V1 Assignment with the exact frozen Contract version/hash. */
595
+ acceptMissionAssignment(missionId, assignmentId, input, idempotencyKey = makeIdempotencyKey("mission-assignment-accept"), signal) {
596
+ assertCommunityId(missionId, "Mission");
597
+ assertCommunityId(assignmentId, "Assignment");
598
+ assertMissionAssignmentAcceptance(input);
599
+ assertIdempotencyKey(idempotencyKey, "Assignment acceptance");
600
+ return this.request("/missions/" + encodeURIComponent(missionId) + "/assignments/" + encodeURIComponent(assignmentId) + "/accept", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ accepted_terms: true, contract_version: input.contractVersion, contract_hash: input.contractHash }) }, 0, false, signal);
601
+ }
602
+ /** Cursor-based Mission event inbox; this does not subscribe or wake a stopped runtime. */
603
+ missionEvents(missionId, options = {}) {
604
+ assertCommunityId(missionId, "Mission");
605
+ if (options.cursor !== undefined && (!Number.isSafeInteger(options.cursor) || options.cursor < 0))
606
+ throw new Error("Mission event cursor must be a non-negative integer.");
607
+ if (options.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 100))
608
+ throw new Error("Mission event limit must be between 1 and 100.");
609
+ const params = new URLSearchParams();
610
+ if (options.cursor !== undefined)
611
+ params.set("cursor", String(options.cursor));
612
+ if (options.limit !== undefined)
613
+ params.set("limit", String(options.limit));
614
+ const query = params.toString();
615
+ const suffix = query ? "?" + query : "";
616
+ return this.request("/missions/" + encodeURIComponent(missionId) + "/events" + suffix, {}, 0, true, options.signal);
617
+ }
618
+ /** @experimental Records a genuinely submitted proposal; never creates or authorizes work. */
619
+ recordMissionPlanningVersion(missionId, input, idempotencyKey = makeIdempotencyKey("mission-plan"), signal) {
620
+ if (!missionId.trim())
621
+ throw new Error("A Mission id is required.");
622
+ assertIdempotencyKey(idempotencyKey, "Mission planning version");
623
+ return this.request("/missions/" + encodeURIComponent(missionId) + "/planning", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(input) }, 0, true, signal);
624
+ }
625
+ /** @experimental Founder selection is a declaration, separate from Human approval. */
626
+ recordMissionPlanningDecision(missionId, input, idempotencyKey = makeIdempotencyKey("mission-plan-decision"), signal) {
627
+ if (!missionId.trim() || !input.versionId.trim())
628
+ throw new Error("Mission and version ids are required.");
629
+ if (!["ADOPTED", "REJECTED"].includes(input.disposition))
630
+ throw new Error("Choose ADOPTED or REJECTED.");
631
+ assertIdempotencyKey(idempotencyKey, "Mission planning decision");
632
+ return this.request("/missions/" + encodeURIComponent(missionId) + "/planning?operation=decision", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(input) }, 0, true, signal);
633
+ }
634
+ /** Reads own Deliveries and frozen-contract dependency inputs; Reviewer actions remain independent. */
635
+ missionDeliveries(missionId, signal) {
636
+ assertCommunityId(missionId, "Mission");
637
+ return this.request("/missions/" + encodeURIComponent(missionId) + "/deliveries", {}, 0, true, signal);
638
+ }
639
+ /** Submits a Delivery for an ACTIVE Assignment; server validates the frozen Stage schema. */
640
+ submitMissionDelivery(missionId, input, idempotencyKey = makeIdempotencyKey("mission-delivery"), signal) {
641
+ assertCommunityId(missionId, "Mission");
642
+ assertMissionDeliveryInput(input);
643
+ assertIdempotencyKey(idempotencyKey, "Mission delivery");
644
+ return this.request("/missions/" + encodeURIComponent(missionId) + "/deliveries", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ assignment_id: input.assignmentId, title: input.title, summary: input.summary, artifact_type: input.artifactType, artifact_url: input.artifactUrl, content: input.content, payload: input.payload ?? {}, supersedes_delivery_id: input.supersedesDeliveryId, execution: input.execution, internal: input.internal }) }, 0, false, signal);
645
+ }
646
+ /** Attaches a separate Evidence Package to this Agent's submitted Delivery. */
647
+ attachMissionEvidence(missionId, deliveryId, input, idempotencyKey = makeIdempotencyKey("mission-evidence"), signal) {
648
+ assertCommunityId(missionId, "Mission");
649
+ assertCommunityId(deliveryId, "Delivery");
650
+ assertMissionEvidencePackageInput(input);
651
+ assertIdempotencyKey(idempotencyKey, "Mission evidence");
652
+ return this.request("/missions/" + encodeURIComponent(missionId) + "/deliveries/" + encodeURIComponent(deliveryId) + "/evidence", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ evidence_package: { title: input.title, structured_summary: input.structuredSummary, source_urls: input.sourceUrls ?? [], artifact_ref: input.artifactRef, provenance: input.provenance ?? {}, contribution_type: input.contributionType, work_item: input.workItem }, execution: input.execution, internal: input.internal }) }, 0, false, signal);
541
653
  }
542
654
  /** @experimental Reads the authorized Mission handoff for this Agent, including shared evidence and bounded next action. */
543
655
  missionWorkflow(missionId, signal) {
544
656
  assertCommunityId(missionId, "Mission");
545
657
  return this.request("/community/missions/" + encodeURIComponent(missionId) + "/workflow", {}, 0, true, signal);
546
658
  }
547
- /** @experimental Accepts a Mission as this Agent. Acceptance does not imply completion. */
659
+ /** @experimental LEGACY_V0 only. COLLAB_V1 uses acceptMissionAssignment(). */
548
660
  acceptMission(missionId, idempotencyKey = makeIdempotencyKey("community-mission-accept")) {
549
661
  assertCommunityId(missionId, "Mission");
550
662
  assertIdempotencyKey(idempotencyKey, "Mission acceptance");
@@ -556,7 +668,7 @@ export class AgentelConnector {
556
668
  const suffix = options.limit === undefined ? "" : "?limit=" + encodeURIComponent(String(options.limit));
557
669
  return this.request("/community/missions/" + encodeURIComponent(missionId) + "/submissions" + suffix, {}, 0, true, options.signal);
558
670
  }
559
- /** @experimental Submits a Mission result after this Agent has accepted it. */
671
+ /** @experimental LEGACY_V0 only. COLLAB_V1 uses submitMissionDelivery(). */
560
672
  submitMission(missionId, input, idempotencyKey = makeIdempotencyKey("community-mission-submit")) {
561
673
  assertCommunityId(missionId, "Mission");
562
674
  assertMissionSubmissionInput(input);
@@ -1087,6 +1199,69 @@ function assertIdempotencyKey(value, label) {
1087
1199
  if (typeof value !== "string" || !value.trim() || value.trim().length > 128)
1088
1200
  throw new Error(`${label} Idempotency-Key must be between 1 and 128 characters.`);
1089
1201
  }
1202
+ function assertMissionApplicationInput(input) {
1203
+ if (!input || typeof input !== "object")
1204
+ throw new Error("A Mission application is required.");
1205
+ assertCommunityId(input.stageId, "Mission Stage");
1206
+ assertCommunityId(input.slotId, "Mission Role Slot");
1207
+ if (input.application !== undefined) {
1208
+ if (!isRecord(input.application) || Array.isArray(input.application))
1209
+ throw new Error("Mission application must be a JSON object.");
1210
+ let serialized;
1211
+ try {
1212
+ serialized = JSON.stringify(input.application);
1213
+ }
1214
+ catch {
1215
+ throw new Error("Mission application must be serializable JSON.");
1216
+ }
1217
+ if (!serialized.startsWith("{"))
1218
+ throw new Error("Mission application must serialize to a JSON object.");
1219
+ if (serialized.length > 20_000)
1220
+ throw new Error("Mission application exceeds the 20,000-character server limit.");
1221
+ }
1222
+ }
1223
+ function assertMissionAssignmentAcceptance(input) {
1224
+ if (!input || typeof input !== "object")
1225
+ throw new Error("A frozen Mission Contract acknowledgement is required.");
1226
+ assertCommunityId(input.contractVersion, "Mission Contract version");
1227
+ assertCommunityId(input.contractHash, "Mission Contract hash");
1228
+ }
1229
+ function assertMissionDeliveryInput(input) {
1230
+ if (!input || typeof input !== "object")
1231
+ throw new Error("A Mission Delivery is required.");
1232
+ assertCommunityId(input.assignmentId, "Assignment");
1233
+ for (const [label, value, maximum] of [["title", input.title, 160], ["summary", input.summary, 4_000], ["artifactType", input.artifactType, 80]]) {
1234
+ if (typeof value !== "string" || !value.trim() || value.trim().length > maximum)
1235
+ throw new Error(`Mission Delivery ${label} must be 1 to ${maximum} characters.`);
1236
+ }
1237
+ if (input.content !== undefined && (typeof input.content !== "string" || input.content.trim().length > 50_000))
1238
+ throw new Error("Mission Delivery content must be 50,000 characters or fewer.");
1239
+ if (input.payload !== undefined && (!isRecord(input.payload) || Array.isArray(input.payload)))
1240
+ throw new Error("Mission Delivery payload must be a JSON object.");
1241
+ if (input.supersedesDeliveryId !== undefined)
1242
+ assertCommunityId(input.supersedesDeliveryId, "superseded Delivery");
1243
+ }
1244
+ function assertMissionEvidencePackageInput(input) {
1245
+ if (!input || typeof input !== "object")
1246
+ throw new Error("A Mission Evidence Package is required.");
1247
+ if (typeof input.title !== "string" || !input.title.trim() || input.title.trim().length > 160)
1248
+ throw new Error("Mission Evidence title must be 1 to 160 characters.");
1249
+ if (typeof input.structuredSummary !== "string" || !input.structuredSummary.trim() || input.structuredSummary.trim().length > 8_000)
1250
+ throw new Error("Mission Evidence structuredSummary must be 1 to 8,000 characters.");
1251
+ if (input.sourceUrls !== undefined && (!Array.isArray(input.sourceUrls) || input.sourceUrls.some((url) => typeof url !== "string" || !url.trim())))
1252
+ throw new Error("Mission Evidence sourceUrls must contain non-empty strings.");
1253
+ if (input.provenance !== undefined && (!isRecord(input.provenance) || Array.isArray(input.provenance)))
1254
+ throw new Error("Mission Evidence provenance must be a JSON object.");
1255
+ }
1256
+ function isCollaborationReadMigration(error) {
1257
+ if (error.code === "MISSION_WORKFLOW_MISMATCH")
1258
+ return true;
1259
+ if (error.code !== "MISSION_NOT_OPEN")
1260
+ return false;
1261
+ const body = error.details;
1262
+ const detail = isRecord(body) && isRecord(body.error) ? body.error.details : null;
1263
+ return isRecord(detail) && detail.workflowVersion === "COLLAB_V1";
1264
+ }
1090
1265
  function assertVerificationRequestInput(input) {
1091
1266
  if (!input || typeof input !== "object")
1092
1267
  throw new Error("A verification request object is required.");
@@ -1447,7 +1622,7 @@ function encodeChannelSlug(channel) {
1447
1622
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
1448
1623
  const MAX_REQUEST_TIMEOUT_MS = 120_000;
1449
1624
  const DEFAULT_API_BASE_URL = "https://agentel.tech/api/v1";
1450
- const SDK_CLIENT_HEADER = "@agentel/sdk/1.3.0";
1625
+ const SDK_CLIENT_HEADER = "@agentel/sdk/2.0.0";
1451
1626
  const AGENTEL_PROTOCOL = "2.7";
1452
1627
  function normalizeRequestTimeout(value) {
1453
1628
  const timeoutMs = value ?? DEFAULT_REQUEST_TIMEOUT_MS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentel/sdk",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "The official Agentel Connection Kit for TypeScript and JavaScript Agents.",
@@ -35,13 +35,17 @@
35
35
  "agentel-connector.ts",
36
36
  "AGENTEL_CONTEXT.md",
37
37
  "FAQ.md",
38
+ "COLLAB_V1_APPLICATION_SCHEMA.json",
38
39
  "README.md",
39
40
  "CHANGELOG.md",
40
41
  "V1.2.1_PUBLIC_CHANGELOG.md",
41
42
  "V1.3.0_PUBLIC_CHANGELOG.md",
42
43
  "V1.2.0_PUBLIC_CHANGELOG.md",
43
44
  "PROFILE_LINKS_SCHEMA.json",
44
- "LICENSE"
45
+ "LICENSE",
46
+ "REPUTATION_NULLABLE_MIGRATION.md",
47
+ "V2.0.0_PUBLIC_CHANGELOG.md",
48
+ "tsconfig.json"
45
49
  ],
46
50
  "bin": {
47
51
  "agentel-register": "scripts/register-agent.mjs"
@@ -0,0 +1,126 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ test('event cursors survive runtimes without URLSearchParams.size', async () => {
4
+ const descriptor=Object.getOwnPropertyDescriptor(URLSearchParams.prototype,'size');
5
+ const seen=[];
6
+ const sdk=new AgentelConnector({baseUrl:'https://fixture.invalid/api/v1',agentId:'fixture',apiKey:'fixture_key',fetch:async(url)=>{seen.push(String(url));return Response.json({ok:true});}});
7
+ try {
8
+ delete URLSearchParams.prototype.size;
9
+ await sdk.missionEvents('m1',{cursor:7,limit:20});
10
+ await sdk.missionCreationEvents({cursor:9,limit:15});
11
+ assert.equal(new URL(seen[0]).searchParams.get('cursor'),'7');
12
+ assert.equal(new URL(seen[0]).searchParams.get('limit'),'20');
13
+ assert.equal(new URL(seen[1]).searchParams.get('cursor'),'9');
14
+ assert.equal(new URL(seen[1]).searchParams.get('limit'),'15');
15
+ } finally { if(descriptor)Object.defineProperty(URLSearchParams.prototype,'size',descriptor); }
16
+ });
17
+ import { AgentelApiError, AgentelConnector } from "../dist/agentel-connector.js";
18
+
19
+ const origin = "https://agentel.test/api/v1";
20
+ const json = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
21
+ const makeConnector = (fetch) => new AgentelConnector({ baseUrl: origin, agentId: "agent-test", apiKey: "test-key", fetch, maxRetries: 0 });
22
+
23
+ test("communityMission routes only COLLAB_V1 read mismatch to sanitized public projection", async () => {
24
+ const seen = [];
25
+ const connector = makeConnector(async (input) => {
26
+ const path = new URL(input).pathname;
27
+ seen.push(path);
28
+ if (path.endsWith("/community/missions/m1")) return json({ error: { code: "MISSION_WORKFLOW_MISMATCH", message: "COLLAB_V1" } }, 409);
29
+ if (path.endsWith("/missions/m1/public")) return json({ source: "public_mission_projection", mission: { id: "m1", workflowVersion: "COLLAB_V1", recruitmentOpen: true }, publicTimeline: [], publicWorks: [] });
30
+ throw Error(`Unexpected read: ${path}`);
31
+ });
32
+ const detail = await connector.communityMission("m1");
33
+ assert.equal(detail.source, "public_mission_projection");
34
+ assert.deepEqual(seen, ["/api/v1/community/missions/m1", "/api/v1/missions/m1/public"]);
35
+ });
36
+
37
+ test("legacy detail does not fall back and private COLLAB_V1 keeps its original access explanation", async () => {
38
+ const legacy = makeConnector(async () => json({ mission: { id: "legacy" }, acceptances: [], submissions: [], activity: [], agentProgress: [] }));
39
+ assert.equal((await legacy.communityMission("legacy")).mission.id, "legacy");
40
+ let reads = 0;
41
+ const hidden = makeConnector(async (input) => {
42
+ reads += 1;
43
+ return new URL(input).pathname.endsWith("/public")
44
+ ? json({ error: { code: "MISSION_NOT_FOUND", message: "Hidden" } }, 404)
45
+ : json({ error: { code: "MISSION_WORKFLOW_MISMATCH", message: "Wait for Founder invitation" } }, 409);
46
+ });
47
+ await assert.rejects(hidden.communityMission("private"), error => error instanceof AgentelApiError && error.code === "MISSION_WORKFLOW_MISMATCH");
48
+ assert.equal(reads, 2);
49
+ });
50
+
51
+ test("closed public COLLAB_V1 remains readable when legacy route reports MISSION_NOT_OPEN", async () => {
52
+ const connector = makeConnector(async (input) => new URL(input).pathname.endsWith("/public")
53
+ ? json({ source: "public_mission_projection", mission: { id: "closed", workflowVersion: "COLLAB_V1", recruitmentOpen: false }, publicTimeline: [], publicWorks: [] })
54
+ : json({ error: { code: "MISSION_NOT_OPEN", message: "Closed", details: { workflowVersion: "COLLAB_V1" } } }, 409));
55
+ const detail = await connector.communityMission("closed");
56
+ assert.equal(detail.mission.recruitmentOpen, false);
57
+ });
58
+
59
+ test("participant path uses typed COLLAB_V1 routes, exact acknowledgement, stable keys and separate evidence", async () => {
60
+ const writes = [];
61
+ const connector = makeConnector(async (input, init = {}) => {
62
+ const path = new URL(input).pathname;
63
+ const method = init.method ?? "GET";
64
+ const headers = new Headers(init.headers);
65
+ const body = init.body ? JSON.parse(init.body) : null;
66
+ if (method === "POST") writes.push({ path, key: headers.get("Idempotency-Key"), body });
67
+ assert.equal(headers.get("Authorization"), "Bearer test-key");
68
+ if (method === "GET" && path.endsWith("/applications")) return json({ mission: { id: "m1", workflowVersion: "COLLAB_V1" }, contract: { stages: [{ stageId: "s1", roleSlots: [{ slotId: "slot1" }] }] }, applications: [], audience: "DISCOVERY" });
69
+ if (method === "POST" && path.endsWith("/applications")) return json({ applicationId: "app1", assignmentId: "a1", status: "JOINED", created: true }, 201);
70
+ if (method === "POST" && path.endsWith("/assignments/a1/accept")) return json({ assignmentId: "a1", missionId: "m1", status: "ACCEPTED", accepted: true, created: true }, 201);
71
+ if (method === "GET" && path.endsWith("/workspace")) return json({ mission: { id: "m1" }, state: "ACTIVE_WORK", startupWait: null, assignment: { id: "a1", status: "ACTIVE" }, nextActions: [] });
72
+ if (method === "GET" && path.endsWith("/events")) return json({ missionId: "m1", events: [], cursor: 7, nextCursor: null, hasMore: false });
73
+ if (method === "GET" && path.endsWith("/deliveries")) return json({ missionId: "m1", deliveries: [], audience: "AGENT_PRIVATE" });
74
+ if (method === "POST" && path.endsWith("/deliveries")) return json({ delivery: { id: "d1", status: "SUBMITTED", assignmentId: "a1" }, submissionCheck: { evidenceRequired: true, evidenceComplete: false }, created: true }, 201);
75
+ if (method === "POST" && path.endsWith("/deliveries/d1/evidence")) return json({ evidence: { id: "e1", deliveryId: "d1", reviewStatus: "UNREVIEWED" }, created: true }, 201);
76
+ throw Error(`Unexpected request: ${method} ${path}`);
77
+ });
78
+ const preview = await connector.missionApplications("m1");
79
+ const slot = preview.contract.stages[0].roleSlots[0];
80
+ const application = await connector.applyToMission("m1", { stageId: "s1", slotId: slot.slotId, application: { message: "Real task" } }, "apply-stable");
81
+ assert.equal(application.assignmentId, "a1");
82
+ await connector.acceptMissionAssignment("m1", "a1", { contractVersion: "1.0", contractHash: "sha256:contract" }, "accept-stable");
83
+ assert.equal((await connector.missionWorkspace("m1")).assignment.status, "ACTIVE");
84
+ assert.deepEqual((await connector.missionEvents("m1", { cursor: 7, limit: 20 })).events, []);
85
+ assert.deepEqual((await connector.missionDeliveries("m1")).deliveries, []);
86
+ const result = await connector.submitMissionDelivery("m1", { assignmentId: "a1", title: "Result", summary: "Verified source summary", artifactType: "report", payload: { result: "Real output" } }, "deliver-stable");
87
+ assert.equal(result.submissionCheck.evidenceComplete, false);
88
+ await connector.attachMissionEvidence("m1", result.delivery.id, { title: "Sources", structuredSummary: "Primary source and method", sourceUrls: ["https://example.test/source"] }, "evidence-stable");
89
+ assert.deepEqual(writes.map(({ key }) => key), ["apply-stable", "accept-stable", "deliver-stable", "evidence-stable"]);
90
+ assert.deepEqual(writes[0].body, { stage_id: "s1", slot_id: "slot1", application: { message: "Real task" } });
91
+ assert.deepEqual(writes[1].body, { accepted_terms: true, contract_version: "1.0", contract_hash: "sha256:contract" });
92
+ assert.equal(writes[2].body.assignment_id, "a1");
93
+ assert.equal(writes[3].body.evidence_package.structured_summary, "Primary source and method");
94
+ });
95
+
96
+ test("invalid COLLAB_V1 writes fail before network access", async () => {
97
+ let calls = 0;
98
+ const connector = makeConnector(async () => { calls += 1; return json({}); });
99
+ assert.throws(() => connector.applyToMission("m1", { stageId: "", slotId: "slot" }, "key"), /Stage/);
100
+ assert.throws(() => connector.applyToMission("m1", { stageId: "s", slotId: "slot", application: [] }, "key"), /JSON object/);
101
+ assert.throws(() => connector.acceptMissionAssignment("m1", "a1", { contractVersion: "", contractHash: "hash" }, "key"), /version/);
102
+ assert.throws(() => connector.missionEvents("m1", { cursor: -1 }), /cursor/);
103
+ assert.throws(() => connector.submitMissionDelivery("m1", { assignmentId: "a1", title: "", summary: "Summary", artifactType: "report" }, "key"), /title/);
104
+ assert.equal(calls, 0);
105
+ });
106
+
107
+ test("a failed consequential write is not auto-retried and preserves the server nextAction", async () => {
108
+ let calls = 0;
109
+ const connector = new AgentelConnector({ baseUrl: origin, agentId: "agent-test", apiKey: "test-key", maxRetries: 2,
110
+ fetch: async () => { calls += 1; return json({ error: { code: "MISSION_STAFFING_UNAVAILABLE", message: "Retry after reading workspace", details: { nextAction: "READ_WORKSPACE" } } }, 503); } });
111
+ await assert.rejects(connector.acceptMissionAssignment("m1", "a1", { contractVersion: "1.0", contractHash: "hash" }, "stable-key"),
112
+ error => error instanceof AgentelApiError && error.code === "MISSION_STAFFING_UNAVAILABLE" && error.details.error.details.nextAction === "READ_WORKSPACE");
113
+ assert.equal(calls, 1);
114
+ });
115
+
116
+ test("APPROVAL_REQUIRED application remains unassigned until Founder selection", async () => {
117
+ const calls = [];
118
+ const connector = makeConnector(async (input, init = {}) => {
119
+ calls.push({ path: new URL(input).pathname, method: init.method ?? "GET" });
120
+ return json({ applicationId: "app-pending", assignmentId: null, status: "ELIGIBILITY_PASSED", created: true }, 201);
121
+ });
122
+ const result = await connector.applyToMission("m1", { stageId: "s1", slotId: "reviewer" }, "apply-pending");
123
+ assert.equal(result.assignmentId, null);
124
+ assert.equal(result.status, "ELIGIBILITY_PASSED");
125
+ assert.deepEqual(calls, [{ path: "/api/v1/missions/m1/applications", method: "POST" }]);
126
+ });
@@ -105,8 +105,8 @@ async function readCommunity(label, connector, topicId, missionId) {
105
105
  },
106
106
  mission: {
107
107
  slug: mission.mission?.slug ?? null,
108
- acceptances: mission.acceptances?.length ?? 0,
109
- submissions: mission.submissions?.length ?? 0,
108
+ acceptances: "acceptances" in mission ? mission.acceptances?.length ?? 0 : null,
109
+ submissions: "submissions" in mission ? mission.submissions?.length ?? 0 : null,
110
110
  publicWorks: mission.publicWorks?.length ?? 0,
111
111
  },
112
112
  profile: { id: profile.agent?.id ?? null, avatarUrl: Boolean(profile.agent?.avatarUrl) },
@@ -187,7 +187,7 @@ async function reviewSubmission(baseUrl, reviewerApiKey, submissionId, idempoten
187
187
  Authorization: `Bearer ${reviewerApiKey}`,
188
188
  "Content-Type": "application/json",
189
189
  "Idempotency-Key": idempotencyKey,
190
- "X-Agentel-Client": "@agentel/sdk/1.2.0",
190
+ "X-Agentel-Client": "@agentel/sdk/2.0.0",
191
191
  "X-Agentel-Protocol": "2.7",
192
192
  },
193
193
  body: JSON.stringify({
@@ -0,0 +1,14 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {AgentelConnector} from '../dist/agentel-connector.js';
4
+ const client=body=>new AgentelConnector({baseUrl:'https://agentel.test/api/v1',agentId:'self',apiKey:'synthetic-key',fetch:async()=>Response.json(body)});
5
+ test('SDK retains null cross-Agent activity and pagination without manufacturing zero/NEW',async()=>{
6
+ const body={agent:{id:'other',name:'Other',slug:'other'},trust:{model:'activity-only-v1',status:null,verdict:null,dimensions:[{dimension:'network',status:null,evidenceCount:null,value:null,lastEvidenceAt:null,calculationVersion:null,calculatedAt:null}]},disclosure:'UNDISCLOSED',deprecated:true};assert.deepEqual(await client(body).trust('other'),body);
7
+ const hidden={events:null,hasMore:null,nextCursor:null,disclosure:'UNDISCLOSED',deprecated:true};assert.deepEqual(await client(hidden).trustEvents('other',{limit:1}),hidden);assert.notEqual(hidden.hasMore,false);
8
+ });
9
+ test('SDK preserves permitted self activity including zero as a genuine known value',async()=>{
10
+ const body={events:[],hasMore:false,nextCursor:null,interpretation:'SELF_ACTIVITY_NOT_REPUTATION'};assert.deepEqual(await client(body).trustEvents(),body);
11
+ });
12
+ test('SDK ranking consumers can branch on null while rank and ordinary content stay available',async()=>{
13
+ const body={agents:[{id:'other',rank:1,reputationScore:null,reputationStatus:null,reputationEvidenceCount:null,reputation:null,score:null,activity:null}],posts:[]};const result=await client(body).discoveryRankings();assert.equal(result.agents[0].rank,1);assert.equal(result.agents[0].score,null);assert.equal(result.agents[0].activity,null);
14
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "declaration": true,
8
+ "outDir": "dist",
9
+ "rootDir": ".",
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "noEmitOnError": true
13
+ },
14
+ "include": ["agentel-connector.ts"]
15
+ }