@agrentingai/paperclip-adapter 0.2.2 → 0.4.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.
@@ -1,6 +1,12 @@
1
1
  // server/src/client.ts
2
2
  var DEFAULT_TIMEOUT_MS = 3e4;
3
3
  var MAX_RETRIES = 3;
4
+ var NonRetryableAgrentingError = class extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "NonRetryableAgrentingError";
8
+ }
9
+ };
4
10
  var AgrentingClient = class {
5
11
  baseUrl;
6
12
  apiKey;
@@ -47,17 +53,22 @@ var AgrentingClient = class {
47
53
  await new Promise((resolve) => setTimeout(resolve, delayMs));
48
54
  continue;
49
55
  }
50
- throw new Error(
56
+ throw new NonRetryableAgrentingError(
51
57
  `Agrenting API ${response.status}: ${text.slice(0, 500)}`
52
58
  );
53
59
  }
54
60
  const envelope = await response.json();
55
61
  if (Array.isArray(envelope.errors) && envelope.errors.length) {
56
- throw new Error(`API errors: ${envelope.errors.join(", ")}`);
62
+ throw new NonRetryableAgrentingError(
63
+ `API errors: ${envelope.errors.join(", ")}`
64
+ );
57
65
  }
58
66
  return envelope.data ?? envelope;
59
67
  } catch (err) {
60
68
  lastError = err instanceof Error ? err : new Error(String(err));
69
+ if (lastError instanceof NonRetryableAgrentingError) {
70
+ throw lastError;
71
+ }
61
72
  if (attempt < MAX_RETRIES) {
62
73
  clearTimeout(timer);
63
74
  const delayMs = Math.min(1e3 * 2 ** attempt, 3e4);
@@ -243,12 +254,23 @@ var AgrentingClient = class {
243
254
  async getAgentProfile(agentDid) {
244
255
  return this.request("GET", `/api/v1/agents/${encodeURIComponent(agentDid)}`);
245
256
  }
246
- /** Hire/bind an agent to your account.
247
- * Returns adapter config so Paperclip can auto-provision the agent.
257
+ /** Create a paid hiring for an agent.
258
+ * Agrenting requires a concrete task, capability, and offered price.
248
259
  */
249
- async hireAgent(agentDid, options = {}) {
250
- const body = {};
251
- if (options.pricingModel) body.pricing_model = options.pricingModel;
260
+ async hireAgent(agentDid, options) {
261
+ const body = {
262
+ task_description: options.taskDescription,
263
+ capability_requested: options.capabilityRequested,
264
+ price: String(options.price),
265
+ delivery_mode: options.deliveryMode ?? "output"
266
+ };
267
+ if (options.repoUrl) body.repo_url = options.repoUrl;
268
+ if (options.repoAccessToken) body.repo_access_token = options.repoAccessToken;
269
+ if (options.clientIdempotencyKey) {
270
+ body.client_idempotency_key = options.clientIdempotencyKey;
271
+ }
272
+ if (options.taskInput) body.task_input = options.taskInput;
273
+ if (options.clientMessage) body.client_message = options.clientMessage;
252
274
  return this.request(
253
275
  "POST",
254
276
  `/api/v1/agents/${encodeURIComponent(agentDid)}/hire`,
@@ -307,13 +329,11 @@ var AgrentingClient = class {
307
329
  );
308
330
  }
309
331
  /** Get messages for a hiring.
310
- * GET /api/v1/hirings/:id/messages
332
+ * The canonical API includes recent messages in GET /api/v1/hirings/:id.
311
333
  */
312
334
  async getHiringMessages(hiringId) {
313
- return this.request(
314
- "GET",
315
- `/api/v1/hirings/${hiringId}/messages`
316
- );
335
+ const hiring = await this.getHiring(hiringId);
336
+ return hiring.messages ?? [];
317
337
  }
318
338
  /** Retry a failed hiring.
319
339
  * POST /api/v1/hirings/:id/retry
@@ -335,6 +355,15 @@ var AgrentingClient = class {
335
355
  async getHiring(hiringId) {
336
356
  return this.request("GET", `/api/v1/hirings/${hiringId}`);
337
357
  }
358
+ /** Cancel an active hiring.
359
+ * POST /api/v1/hirings/:id/cancel
360
+ */
361
+ async cancelHiring(hiringId) {
362
+ return this.request(
363
+ "POST",
364
+ `/api/v1/hirings/${hiringId}/cancel`
365
+ );
366
+ }
338
367
  /** List hirings for the authenticated agent.
339
368
  * GET /api/v1/hirings
340
369
  */
@@ -342,9 +371,16 @@ var AgrentingClient = class {
342
371
  const params = new URLSearchParams();
343
372
  if (options.status) params.set("status", options.status);
344
373
  if (options.limit) params.set("limit", String(options.limit));
345
- if (options.offset) params.set("offset", String(options.offset));
374
+ if (options.offset) {
375
+ const perPage = options.limit ?? 20;
376
+ params.set("page", String(Math.floor(options.offset / perPage) + 1));
377
+ }
346
378
  const query = params.toString() ? `?${params}` : "";
347
- return this.request("GET", `/api/v1/hirings${query}`);
379
+ const result = await this.request(
380
+ "GET",
381
+ `/api/v1/hirings${query}`
382
+ );
383
+ return result.hirings;
348
384
  }
349
385
  /** List agents filtered by capability for auto-select.
350
386
  * GET /api/v1/agents?capability=X
@@ -357,6 +393,493 @@ var AgrentingClient = class {
357
393
  }
358
394
  };
359
395
 
396
+ // server/src/paperclip.ts
397
+ var type = "agrenting";
398
+ var label = "Agrenting";
399
+ var agentConfigurationDoc = `# agrenting agent configuration
400
+
401
+ Adapter: agrenting
402
+
403
+ Use when:
404
+ - A Paperclip agent should delegate each heartbeat to a remote agent hired from Agrenting.
405
+ - The operator has an Agrenting user API token and sufficient ledger balance.
406
+
407
+ Core fields:
408
+ - agrentingUrl (required): Agrenting base URL, normally https://agrenting.com
409
+ - apiKey (required, secret): Agrenting user API token (ap_...)
410
+ - agentDid (required): DID of the marketplace agent to hire
411
+ - capabilityRequested (optional): defaults to the first capability on the agent profile
412
+ - price (optional): defaults to the agent's current base price
413
+ - timeoutSec (optional): maximum time to poll a hiring, default 600
414
+ - pollIntervalMs (optional): hiring status poll interval, default 2000
415
+ - deliveryMode (optional): output by default; push requires repository access
416
+ - repoUrl (optional): repository URL used only when deliveryMode is push
417
+
418
+ Recommended API-key scopes:
419
+ - agents:discover, agents:read, hire:create, hirings:read, hirings:cancel
420
+ - add balance:read and artifacts:read when sharing the key with Apps/Claude
421
+ - deposits:create and account:read/account:write are optional elevated scopes
422
+ - set max_price_per_hire on the key to cap paid actions
423
+
424
+ Execution:
425
+ - Each Paperclip run creates one canonical Agrenting hiring.
426
+ - The Paperclip run id is sent as client_idempotency_key so safe retries deduplicate.
427
+ - Paperclip polls the hiring until it completes, fails, is cancelled, or times out.
428
+ - Push delivery can use a repository URL from Paperclip and an Agrenting-stored
429
+ GitHub token. The adapter never stores a repository token in agent config.
430
+ `;
431
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
432
+ "completed",
433
+ "failed",
434
+ "cancelled",
435
+ "disputed",
436
+ "refunded"
437
+ ]);
438
+ var DEFAULT_TIMEOUT_SEC = 600;
439
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
440
+ var MAX_TASK_DESCRIPTION_LENGTH = 5e3;
441
+ function asRecord(value) {
442
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
443
+ return null;
444
+ }
445
+ return value;
446
+ }
447
+ function nonEmpty(value) {
448
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
449
+ }
450
+ function positiveNumber(value, fallback) {
451
+ const parsed = typeof value === "number" ? value : Number(value);
452
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
453
+ }
454
+ function configFrom(raw) {
455
+ return {
456
+ agrentingUrl: nonEmpty(raw.agrentingUrl) ?? "https://agrenting.com",
457
+ apiKey: nonEmpty(raw.apiKey) ?? "",
458
+ agentDid: nonEmpty(raw.agentDid) ?? "",
459
+ capabilityRequested: nonEmpty(raw.capabilityRequested) ?? void 0,
460
+ price: nonEmpty(raw.price) ?? void 0,
461
+ timeoutSec: positiveNumber(raw.timeoutSec, DEFAULT_TIMEOUT_SEC),
462
+ pollIntervalMs: positiveNumber(
463
+ raw.pollIntervalMs,
464
+ DEFAULT_POLL_INTERVAL_MS
465
+ ),
466
+ deliveryMode: raw.deliveryMode === "push" ? "push" : "output",
467
+ repoUrl: nonEmpty(raw.repoUrl) ?? void 0
468
+ };
469
+ }
470
+ function taskDescriptionFrom(ctx) {
471
+ const context = ctx.context;
472
+ const title = nonEmpty(context.taskTitle) ?? nonEmpty(context.issueTitle) ?? nonEmpty(context.title);
473
+ const body = nonEmpty(context.paperclipTaskMarkdown) ?? nonEmpty(context.taskBody) ?? nonEmpty(context.taskDescription) ?? nonEmpty(context.issueDescription) ?? nonEmpty(context.prompt) ?? nonEmpty(context.input);
474
+ let description = [title, body].filter(Boolean).join("\n\n").trim();
475
+ if (!description) {
476
+ description = `Continue the assigned Paperclip work for ${ctx.agent.name}.`;
477
+ }
478
+ if (description.length <= MAX_TASK_DESCRIPTION_LENGTH) return description;
479
+ return `${description.slice(0, MAX_TASK_DESCRIPTION_LENGTH - 32)}
480
+
481
+ [truncated by Paperclip adapter]`;
482
+ }
483
+ function capabilityFrom(config, context, profile) {
484
+ return nonEmpty(config.capabilityRequested) ?? nonEmpty(context.capabilityRequested) ?? nonEmpty(context.capability) ?? profile.capabilities.find((capability) => capability.trim().length > 0) ?? null;
485
+ }
486
+ function priceFrom(config, context, profile) {
487
+ return nonEmpty(config.price) ?? nonEmpty(context.price) ?? nonEmpty(context.maxPrice) ?? nonEmpty(profile.base_price);
488
+ }
489
+ function repoUrlFrom(ctx) {
490
+ const workspace = asRecord(ctx.context.paperclipWorkspace);
491
+ return nonEmpty(configFrom(ctx.config).repoUrl) ?? nonEmpty(workspace?.repoUrl) ?? void 0;
492
+ }
493
+ function taskInputFrom(ctx) {
494
+ const input = {
495
+ paperclip_run_id: ctx.runId,
496
+ paperclip_agent_id: ctx.agent.id,
497
+ paperclip_company_id: ctx.agent.companyId
498
+ };
499
+ const mappings = [
500
+ ["paperclip_issue_id", ctx.context.issueId ?? ctx.context.taskId],
501
+ ["paperclip_project_id", ctx.context.projectId],
502
+ ["paperclip_wake_reason", ctx.context.wakeReason]
503
+ ];
504
+ for (const [key, value] of mappings) {
505
+ const normalized = nonEmpty(value);
506
+ if (normalized) input[key] = normalized;
507
+ }
508
+ const wake = asRecord(ctx.context.paperclipWake);
509
+ if (wake) input.paperclip_wake = wake;
510
+ return input;
511
+ }
512
+ function outputText(hiring) {
513
+ const output = hiring.task_output;
514
+ if (typeof output === "string" && output.trim()) return output;
515
+ const record = asRecord(output);
516
+ if (record) {
517
+ for (const key of ["result", "output", "text", "summary"]) {
518
+ const direct = nonEmpty(record[key]);
519
+ if (direct) return direct;
520
+ }
521
+ return JSON.stringify(record, null, 2);
522
+ }
523
+ return `Agrenting hiring ${hiring.id} completed.`;
524
+ }
525
+ function firstLine(value) {
526
+ return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "Agrenting hiring completed";
527
+ }
528
+ function sleep(ms) {
529
+ return new Promise((resolve) => setTimeout(resolve, ms));
530
+ }
531
+ function resultForFailure(hiring, message) {
532
+ return {
533
+ exitCode: 1,
534
+ signal: null,
535
+ timedOut: false,
536
+ errorMessage: message,
537
+ errorCode: `agrenting_hiring_${hiring.status}`,
538
+ provider: "agrenting",
539
+ biller: "agrenting",
540
+ sessionParams: { hiringId: hiring.id },
541
+ sessionDisplayId: hiring.id,
542
+ resultJson: {
543
+ hiringId: hiring.id,
544
+ status: hiring.status,
545
+ failedReason: hiring.failed_reason ?? null
546
+ }
547
+ };
548
+ }
549
+ async function executePaperclip(ctx) {
550
+ const config = configFrom(ctx.config);
551
+ if (!config.apiKey || !config.agentDid) {
552
+ return {
553
+ exitCode: 1,
554
+ signal: null,
555
+ timedOut: false,
556
+ errorCode: "agrenting_config_invalid",
557
+ errorMessage: "Agrenting requires apiKey and agentDid."
558
+ };
559
+ }
560
+ const client = new AgrentingClient(config);
561
+ try {
562
+ const profile = await client.getAgentProfile(config.agentDid);
563
+ const capability = capabilityFrom(config, ctx.context, profile);
564
+ const price = priceFrom(config, ctx.context, profile);
565
+ if (!capability) {
566
+ throw new Error(
567
+ "No capabilityRequested was configured and the Agrenting agent profile has no capabilities."
568
+ );
569
+ }
570
+ if (!price) {
571
+ throw new Error(
572
+ "No price was configured and the Agrenting agent profile has no base_price."
573
+ );
574
+ }
575
+ const taskDescription = taskDescriptionFrom(ctx);
576
+ await ctx.onLog(
577
+ "stdout",
578
+ `[agrenting] Hiring ${config.agentDid} for ${capability} at ${price}
579
+ `
580
+ );
581
+ const created = await client.hireAgent(config.agentDid, {
582
+ taskDescription,
583
+ capabilityRequested: capability,
584
+ price,
585
+ deliveryMode: config.deliveryMode,
586
+ clientIdempotencyKey: ctx.runId,
587
+ taskInput: taskInputFrom(ctx),
588
+ repoUrl: repoUrlFrom(ctx)
589
+ });
590
+ const hiringId = created.hiring.id;
591
+ await ctx.onMeta?.({
592
+ adapterType: type,
593
+ command: "POST /api/v1/agents/:did/hire",
594
+ context: { hiringId, agentDid: config.agentDid, capability, price }
595
+ });
596
+ await ctx.onLog(
597
+ "stdout",
598
+ `[agrenting] Hiring ${hiringId} created with status ${created.hiring.status}
599
+ `
600
+ );
601
+ const timeoutMs = (config.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
602
+ const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
603
+ const deadline = Date.now() + timeoutMs;
604
+ let hiring = created.hiring;
605
+ let lastStatus = hiring.status;
606
+ while (!TERMINAL_STATUSES.has(hiring.status) && Date.now() < deadline) {
607
+ await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
608
+ hiring = await client.getHiring(hiringId);
609
+ if (hiring.status !== lastStatus) {
610
+ lastStatus = hiring.status;
611
+ await ctx.onLog(
612
+ "stdout",
613
+ `[agrenting] Hiring ${hiringId} is ${hiring.status}
614
+ `
615
+ );
616
+ }
617
+ }
618
+ if (!TERMINAL_STATUSES.has(hiring.status)) {
619
+ try {
620
+ await client.cancelHiring(hiringId);
621
+ await ctx.onLog(
622
+ "stderr",
623
+ `[agrenting] Hiring ${hiringId} timed out and was cancelled
624
+ `
625
+ );
626
+ } catch (cancelError) {
627
+ await ctx.onLog(
628
+ "stderr",
629
+ `[agrenting] Hiring ${hiringId} timed out; cancellation failed: ${cancelError instanceof Error ? cancelError.message : String(cancelError)}
630
+ `
631
+ );
632
+ }
633
+ return {
634
+ exitCode: null,
635
+ signal: null,
636
+ timedOut: true,
637
+ errorCode: "agrenting_hiring_timeout",
638
+ errorMessage: `Agrenting hiring ${hiringId} timed out after ${config.timeoutSec ?? DEFAULT_TIMEOUT_SEC}s.`,
639
+ provider: "agrenting",
640
+ biller: "agrenting",
641
+ sessionParams: { hiringId },
642
+ sessionDisplayId: hiringId
643
+ };
644
+ }
645
+ if (hiring.status !== "completed") {
646
+ return resultForFailure(
647
+ hiring,
648
+ hiring.failed_reason ?? `Agrenting hiring ended with status ${hiring.status}.`
649
+ );
650
+ }
651
+ const output = outputText(hiring);
652
+ await ctx.onLog("stdout", `${output}
653
+ `);
654
+ const numericPrice = Number(hiring.price ?? price);
655
+ return {
656
+ exitCode: 0,
657
+ signal: null,
658
+ timedOut: false,
659
+ provider: "agrenting",
660
+ biller: "agrenting",
661
+ billingType: "fixed",
662
+ costUsd: Number.isFinite(numericPrice) ? numericPrice : null,
663
+ sessionParams: { hiringId },
664
+ sessionDisplayId: hiringId,
665
+ summary: firstLine(output),
666
+ resultJson: {
667
+ hiringId,
668
+ status: hiring.status,
669
+ taskOutput: hiring.task_output ?? null
670
+ }
671
+ };
672
+ } catch (error) {
673
+ const message = error instanceof Error ? error.message : String(error);
674
+ const transient = /\b(429|5\d\d)\b|rate.?limit|temporar|timeout/i.test(message);
675
+ await ctx.onLog("stderr", `[agrenting] ${message}
676
+ `);
677
+ return {
678
+ exitCode: 1,
679
+ signal: null,
680
+ timedOut: false,
681
+ errorCode: "agrenting_hiring_failed",
682
+ errorFamily: transient ? "transient_upstream" : null,
683
+ errorMessage: message,
684
+ provider: "agrenting",
685
+ biller: "agrenting"
686
+ };
687
+ }
688
+ }
689
+ function summarizeChecks(checks) {
690
+ if (checks.some((check) => check.level === "error")) return "fail";
691
+ if (checks.some((check) => check.level === "warn")) return "warn";
692
+ return "pass";
693
+ }
694
+ async function testPaperclipEnvironment(ctx) {
695
+ const config = configFrom(ctx.config);
696
+ const checks = [];
697
+ let parsedUrl = null;
698
+ try {
699
+ parsedUrl = new URL(config.agrentingUrl);
700
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
701
+ parsedUrl = null;
702
+ }
703
+ } catch {
704
+ parsedUrl = null;
705
+ }
706
+ if (!parsedUrl) {
707
+ checks.push({
708
+ code: "agrenting_url_invalid",
709
+ level: "error",
710
+ message: "agrentingUrl must be an http:// or https:// URL."
711
+ });
712
+ }
713
+ if (!config.apiKey) {
714
+ checks.push({
715
+ code: "agrenting_api_key_missing",
716
+ level: "error",
717
+ message: "Agrenting requires a user API token.",
718
+ hint: "Create an ap_... token in Agrenting and store it as a Paperclip secret."
719
+ });
720
+ }
721
+ if (!config.agentDid) {
722
+ checks.push({
723
+ code: "agrenting_agent_did_missing",
724
+ level: "error",
725
+ message: "Agrenting requires agentDid."
726
+ });
727
+ }
728
+ if (!checks.some((check) => check.level === "error")) {
729
+ const client = new AgrentingClient(config);
730
+ let connectionOk = false;
731
+ try {
732
+ await client.listHirings({ limit: 1 });
733
+ connectionOk = true;
734
+ checks.push({
735
+ code: "agrenting_connection_ok",
736
+ level: "info",
737
+ message: "Connected to the authenticated Agrenting hiring API."
738
+ });
739
+ } catch (error) {
740
+ checks.push({
741
+ code: "agrenting_connection_failed",
742
+ level: "error",
743
+ message: "Could not access the authenticated Agrenting hiring API.",
744
+ detail: error instanceof Error ? error.message : String(error)
745
+ });
746
+ }
747
+ if (connectionOk) {
748
+ try {
749
+ const profile = await client.getAgentProfile(config.agentDid);
750
+ checks.push({
751
+ code: "agrenting_agent_profile_ok",
752
+ level: "info",
753
+ message: `Found Agrenting agent ${profile.name} (${profile.did}).`,
754
+ detail: `${profile.capabilities.length} capabilities; base price ${profile.base_price ?? "not reported"}.`
755
+ });
756
+ } catch (error) {
757
+ checks.push({
758
+ code: "agrenting_agent_profile_failed",
759
+ level: "error",
760
+ message: `Could not load Agrenting agent ${config.agentDid}.`,
761
+ detail: error instanceof Error ? error.message : String(error)
762
+ });
763
+ }
764
+ }
765
+ }
766
+ return {
767
+ adapterType: ctx.adapterType,
768
+ status: summarizeChecks(checks),
769
+ checks,
770
+ testedAt: (/* @__PURE__ */ new Date()).toISOString()
771
+ };
772
+ }
773
+ function getPaperclipConfigSchema() {
774
+ return {
775
+ fields: [
776
+ {
777
+ key: "agrentingUrl",
778
+ label: "Agrenting URL",
779
+ type: "text",
780
+ required: true,
781
+ default: "https://agrenting.com",
782
+ hint: "Base URL of the Agrenting platform."
783
+ },
784
+ {
785
+ key: "apiKey",
786
+ label: "API token",
787
+ type: "text",
788
+ required: true,
789
+ hint: "Agrenting user API token (ap_...). Stored as a Paperclip secret.",
790
+ meta: { secret: true }
791
+ },
792
+ {
793
+ key: "agentDid",
794
+ label: "Agent DID",
795
+ type: "text",
796
+ required: true,
797
+ hint: "Marketplace agent DID, for example did:agrenting:code-reviewer."
798
+ },
799
+ {
800
+ key: "capabilityRequested",
801
+ label: "Capability",
802
+ type: "text",
803
+ hint: "Optional default. Falls back to the first capability on the agent profile."
804
+ },
805
+ {
806
+ key: "price",
807
+ label: "Price per hiring (USD)",
808
+ type: "text",
809
+ hint: "Optional. Falls back to the agent's current base price."
810
+ },
811
+ {
812
+ key: "timeoutSec",
813
+ label: "Timeout seconds",
814
+ type: "number",
815
+ default: DEFAULT_TIMEOUT_SEC
816
+ },
817
+ {
818
+ key: "pollIntervalMs",
819
+ label: "Poll interval milliseconds",
820
+ type: "number",
821
+ default: DEFAULT_POLL_INTERVAL_MS
822
+ },
823
+ {
824
+ key: "repoUrl",
825
+ label: "Repository URL",
826
+ type: "text",
827
+ hint: "Optional push target. Push delivery uses a GitHub token already stored in Agrenting."
828
+ },
829
+ {
830
+ key: "deliveryMode",
831
+ label: "Delivery mode",
832
+ type: "select",
833
+ default: "output",
834
+ options: [
835
+ { value: "output", label: "Return output" },
836
+ { value: "push", label: "Push to repository" }
837
+ ],
838
+ hint: "Use output unless the hiring also has repository credentials."
839
+ }
840
+ ]
841
+ };
842
+ }
843
+ var paperclipSessionCodec = {
844
+ deserialize(raw) {
845
+ if (typeof raw === "string") {
846
+ try {
847
+ return asRecord(JSON.parse(raw));
848
+ } catch {
849
+ return null;
850
+ }
851
+ }
852
+ return asRecord(raw);
853
+ },
854
+ serialize(params) {
855
+ return params;
856
+ },
857
+ getDisplayId(params) {
858
+ return nonEmpty(params?.hiringId);
859
+ },
860
+ encode(state) {
861
+ return JSON.stringify(state ?? null);
862
+ },
863
+ decode(blob) {
864
+ if (!blob) return null;
865
+ try {
866
+ return JSON.parse(blob);
867
+ } catch {
868
+ return null;
869
+ }
870
+ }
871
+ };
872
+ function createCanonicalServerAdapter() {
873
+ return {
874
+ type,
875
+ execute: executePaperclip,
876
+ testEnvironment: testPaperclipEnvironment,
877
+ sessionCodec: paperclipSessionCodec,
878
+ getConfigSchema: getPaperclipConfigSchema,
879
+ agentConfigurationDoc
880
+ };
881
+ }
882
+
360
883
  // server/src/crypto.ts
361
884
  async function verifyWebhookSignature(rawBody, signature, secret) {
362
885
  const crypto = await import("crypto");
@@ -841,11 +1364,11 @@ async function startWebhookListener(config) {
841
1364
  if (resolvedTaskId) {
842
1365
  const pending = pendingTasks.get(resolvedTaskId);
843
1366
  if (pending && !pending.settled) {
844
- const status = payload.status ?? pending.status;
845
- pending.status = status;
1367
+ const status2 = payload.status ?? pending.status;
1368
+ pending.status = status2;
846
1369
  pending.progressPercent = payload.progress_percent ?? pending.progressPercent;
847
1370
  pending.progressMessage = payload.progress_message ?? pending.progressMessage;
848
- if (status === "completed") {
1371
+ if (status2 === "completed") {
849
1372
  pending.settled = true;
850
1373
  pending.resolve({
851
1374
  success: true,
@@ -853,7 +1376,7 @@ async function startWebhookListener(config) {
853
1376
  taskId: resolvedTaskId,
854
1377
  durationMs: Date.now() - pending.startedAt
855
1378
  });
856
- } else if (status === "failed") {
1379
+ } else if (status2 === "failed") {
857
1380
  pending.settled = true;
858
1381
  pending.resolve({
859
1382
  success: false,
@@ -861,7 +1384,7 @@ async function startWebhookListener(config) {
861
1384
  taskId: resolvedTaskId,
862
1385
  durationMs: Date.now() - pending.startedAt
863
1386
  });
864
- } else if (status === "cancelled") {
1387
+ } else if (status2 === "cancelled") {
865
1388
  pending.settled = true;
866
1389
  pending.resolve({
867
1390
  success: false,
@@ -1092,9 +1615,9 @@ async function cancelTask(config, taskId) {
1092
1615
  var TASK_MAX_RETRIES = 2;
1093
1616
  var TASK_RETRY_BASE_DELAY_MS = 1e3;
1094
1617
  var TASK_RETRY_MAX_DELAY_MS = 3e4;
1095
- async function hireAgent(config, agentDid) {
1618
+ async function hireAgent(config, agentDid, options) {
1096
1619
  const client = new AgrentingClient(config);
1097
- return client.hireAgent(agentDid);
1620
+ return client.hireAgent(agentDid, options);
1098
1621
  }
1099
1622
  async function getAgentProfile(config, agentDid) {
1100
1623
  const client = new AgrentingClient(config);
@@ -1136,6 +1659,10 @@ async function listHirings(config, options) {
1136
1659
  const client = new AgrentingClient(config);
1137
1660
  return client.listHirings(options);
1138
1661
  }
1662
+ async function cancelHiring(config, hiringId) {
1663
+ const client = new AgrentingClient(config);
1664
+ return client.cancelHiring(hiringId);
1665
+ }
1139
1666
  async function autoSelectAgent(config, options) {
1140
1667
  const client = new AgrentingClient(config);
1141
1668
  const capabilities = await client.listCapabilities();
@@ -1161,7 +1688,8 @@ async function autoSelectAgent(config, options) {
1161
1688
  const minRep = options.minReputation;
1162
1689
  filtered = filtered.filter((a) => {
1163
1690
  if (!a.reputation_score) return false;
1164
- return a.reputation_score >= minRep;
1691
+ const reputation = Number(a.reputation_score);
1692
+ return Number.isFinite(reputation) && reputation >= minRep;
1165
1693
  });
1166
1694
  }
1167
1695
  if (filtered.length === 0) {
@@ -1172,15 +1700,17 @@ async function autoSelectAgent(config, options) {
1172
1700
  const sortBy = options.sortBy ?? "reputation_score";
1173
1701
  if (options.preferAvailable ?? true) {
1174
1702
  filtered.sort((a, b) => {
1175
- const aAvail = a.availability_status === "available" ? 0 : 1;
1176
- const bAvail = b.availability_status === "available" ? 0 : 1;
1703
+ const aAvail = (a.availability_status ?? a.availability ?? a.status) === "available" ? 0 : 1;
1704
+ const bAvail = (b.availability_status ?? b.availability ?? b.status) === "available" ? 0 : 1;
1177
1705
  if (aAvail !== bAvail) return aAvail - bAvail;
1178
1706
  return 0;
1179
1707
  });
1180
1708
  }
1181
1709
  filtered.sort((a, b) => {
1182
1710
  if (sortBy === "reputation_score") {
1183
- return (b.reputation_score ?? 0) - (a.reputation_score ?? 0);
1711
+ const aReputation = Number(a.reputation_score ?? 0);
1712
+ const bReputation = Number(b.reputation_score ?? 0);
1713
+ return (Number.isFinite(bReputation) ? bReputation : 0) - (Number.isFinite(aReputation) ? aReputation : 0);
1184
1714
  }
1185
1715
  if (sortBy === "base_price") {
1186
1716
  const aPrice = parseFloat(a.base_price ?? "999999");
@@ -1188,14 +1718,19 @@ async function autoSelectAgent(config, options) {
1188
1718
  return aPrice - bPrice;
1189
1719
  }
1190
1720
  if (sortBy === "availability") {
1191
- const aAvail = a.availability_status === "available" ? 0 : 1;
1192
- const bAvail = b.availability_status === "available" ? 0 : 1;
1721
+ const aAvail = (a.availability_status ?? a.availability ?? a.status) === "available" ? 0 : 1;
1722
+ const bAvail = (b.availability_status ?? b.availability ?? b.status) === "available" ? 0 : 1;
1193
1723
  return aAvail - bAvail;
1194
1724
  }
1195
1725
  return 0;
1196
1726
  });
1197
1727
  const selectedAgent = filtered[0];
1198
- const hireResult = await client.hireAgent(selectedAgent.did);
1728
+ const hireResult = await client.hireAgent(selectedAgent.did, {
1729
+ taskDescription: options.taskDescription,
1730
+ capabilityRequested: options.capability,
1731
+ price: selectedAgent.base_price ?? options.maxPrice ?? "0",
1732
+ deliveryMode: "output"
1733
+ });
1199
1734
  return {
1200
1735
  ...hireResult,
1201
1736
  selectedAgent
@@ -1262,12 +1797,63 @@ function processIncomingMessage2(message) {
1262
1797
  const senderName = message.sender_name ?? "Agent";
1263
1798
  return formatAgentResponse(senderName, message.content);
1264
1799
  }
1800
+ async function detectModel(config) {
1801
+ if (!config.agentDid) return null;
1802
+ try {
1803
+ const profile = await getAgentProfile(config, config.agentDid);
1804
+ const provider = profile.ai_provider ?? null;
1805
+ const model = profile.ai_model ?? null;
1806
+ if (provider && model) return { provider, model };
1807
+ } catch {
1808
+ }
1809
+ return null;
1810
+ }
1811
+ async function listSkills(config) {
1812
+ const profile = await getAgentProfile(config, config.agentDid);
1813
+ const capabilities = profile.capabilities ?? [];
1814
+ return capabilities.map((cap) => ({
1815
+ id: `${config.agentDid}:${cap}`,
1816
+ name: cap,
1817
+ description: `Capability "${cap}" of agent ${config.agentDid}`
1818
+ }));
1819
+ }
1820
+ async function syncSkills(config, existing) {
1821
+ const remote = await listSkills(config);
1822
+ const remoteIds = new Set(remote.map((s) => s.id));
1823
+ const existingIds = new Set(existing.map((s) => s.id));
1824
+ return {
1825
+ added: remote.filter((s) => !existingIds.has(s.id)),
1826
+ removed: existing.filter((s) => !remoteIds.has(s.id))
1827
+ };
1828
+ }
1829
+ var sessionCodec = paperclipSessionCodec;
1830
+ async function invoke(config, params) {
1831
+ return execute(config, params);
1832
+ }
1833
+ async function status(config, taskId) {
1834
+ const progress = await getTaskProgress(config, taskId);
1835
+ return {
1836
+ status: progress.status,
1837
+ progressPercent: progress.progressPercent,
1838
+ progressMessage: progress.progressMessage
1839
+ };
1840
+ }
1841
+ async function cancel(config, taskId) {
1842
+ const result = await cancelTask(config, taskId);
1843
+ if (!result.success) {
1844
+ throw new Error(result.error ?? "Failed to cancel task");
1845
+ }
1846
+ }
1265
1847
  function createServerAdapter() {
1848
+ const canonical = createCanonicalServerAdapter();
1266
1849
  return {
1850
+ ...canonical,
1267
1851
  name: "agrenting",
1268
- execute,
1269
- testEnvironment,
1270
- getConfigSchema,
1852
+ // Legacy helpers remain available under explicit names. The canonical
1853
+ // Paperclip keys above use AdapterExecutionContext and structured tests.
1854
+ legacyExecute: execute,
1855
+ legacyTestEnvironment: testEnvironment,
1856
+ getLegacyConfigSchema: getConfigSchema,
1271
1857
  startWebhookListener,
1272
1858
  stopWebhookListener,
1273
1859
  registerWebhook,
@@ -1285,6 +1871,7 @@ function createServerAdapter() {
1285
1871
  sendMessageToHiring,
1286
1872
  getHiringMessages,
1287
1873
  retryHiring,
1874
+ cancelHiring,
1288
1875
  getHiring,
1289
1876
  listHirings,
1290
1877
  autoSelectAgent,
@@ -1294,19 +1881,64 @@ function createServerAdapter() {
1294
1881
  getBalance,
1295
1882
  getTransactions,
1296
1883
  deposit,
1297
- withdraw
1884
+ withdraw,
1885
+ // Legacy pre-0.4 contract additions:
1886
+ invoke,
1887
+ status,
1888
+ cancel,
1889
+ legacyDetectModel: detectModel,
1890
+ legacyListSkills: listSkills,
1891
+ legacySyncSkills: syncSkills
1298
1892
  };
1299
1893
  }
1894
+
1895
+ // server/src/apps-v2.ts
1896
+ var agrentingAppGalleryEntry = {
1897
+ key: "agrenting",
1898
+ name: "Agrenting",
1899
+ logoUrl: "https://www.google.com/s2/favicons?domain=agrenting.com&sz=64",
1900
+ tagline: "Discover and hire governed remote AI agents.",
1901
+ description: "Give Paperclip agents governed access to discover, hire, monitor, and cancel remote agents in the Agrenting marketplace.",
1902
+ authKind: "api_key",
1903
+ transportTemplate: {
1904
+ transport: "remote_http",
1905
+ url: "https://agrenting.com/mcp/hirer"
1906
+ },
1907
+ credentialFields: [
1908
+ {
1909
+ label: "Agrenting API key",
1910
+ configPath: "credentials.authorization",
1911
+ helpUrl: "https://agrenting.com/dashboard/api-keys",
1912
+ required: true,
1913
+ placement: "header",
1914
+ key: "Authorization",
1915
+ prefix: "Bearer "
1916
+ }
1917
+ ],
1918
+ recommendedDefaults: {
1919
+ access: "all_agents",
1920
+ askFirstRiskLevels: ["write", "destructive"]
1921
+ },
1922
+ urlPatterns: ["https://agrenting.com/mcp/hirer*"]
1923
+ };
1300
1924
  export {
1301
1925
  AgrentingClient,
1302
1926
  MAX_POLLS,
1303
1927
  POLL_INTERVALS_MS,
1928
+ agentConfigurationDoc,
1929
+ agrentingAppGalleryEntry,
1304
1930
  autoSelectAgent,
1305
1931
  canSubmitTask,
1932
+ cancel,
1933
+ cancelHiring,
1934
+ cancelTask,
1306
1935
  checkBalance,
1307
1936
  createServerAdapter,
1308
1937
  createWebhookHandler,
1309
1938
  deregisterWebhook,
1939
+ detectModel,
1940
+ execute,
1941
+ executePaperclip,
1310
1942
  executeWithRetry,
1311
1943
  formatAgentResponse,
1312
1944
  formatInsufficientBalanceComment,
@@ -1315,13 +1947,20 @@ export {
1315
1947
  getActiveTaskMappings,
1316
1948
  getAgentProfile,
1317
1949
  getBackoffMs,
1950
+ getConfigSchema,
1318
1951
  getHiring,
1319
1952
  getHiringMessages,
1953
+ getPaperclipConfigSchema,
1320
1954
  getTaskMessages,
1955
+ getTaskProgress,
1321
1956
  getWebhookGracePeriodMs,
1322
1957
  hireAgent,
1958
+ invoke,
1959
+ label,
1323
1960
  listCapabilities,
1324
1961
  listHirings,
1962
+ listSkills,
1963
+ paperclipSessionCodec,
1325
1964
  pollTaskUntilDone,
1326
1965
  processIncomingMessage,
1327
1966
  reassignTask,
@@ -1330,6 +1969,12 @@ export {
1330
1969
  retryHiring,
1331
1970
  sendMessageToHiring,
1332
1971
  sendMessageToTask,
1972
+ sessionCodec,
1973
+ status,
1974
+ syncSkills,
1975
+ testEnvironment,
1976
+ testPaperclipEnvironment,
1977
+ type,
1333
1978
  unregisterTaskMapping,
1334
1979
  verifyWebhookSignature
1335
1980
  };