@loomwork/plugin-a2a 0.0.0-stage → 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.
Files changed (45) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +80 -2
  4. package/cards.d.ts +1 -0
  5. package/dist/A2AClientEffect.d.ts +11 -0
  6. package/dist/A2AClientEffect.d.ts.map +1 -0
  7. package/dist/A2AClientEffect.js +89 -0
  8. package/dist/A2AClientEffect.js.map +1 -0
  9. package/dist/A2AEffect.d.ts +13 -0
  10. package/dist/A2AEffect.d.ts.map +1 -0
  11. package/dist/A2AEffect.js +20 -0
  12. package/dist/A2AEffect.js.map +1 -0
  13. package/dist/A2APeerClient.d.ts +126 -0
  14. package/dist/A2APeerClient.d.ts.map +1 -0
  15. package/dist/A2APeerClient.js +167 -0
  16. package/dist/A2APeerClient.js.map +1 -0
  17. package/dist/A2AServer.d.ts +78 -0
  18. package/dist/A2AServer.d.ts.map +1 -0
  19. package/dist/A2AServer.js +239 -0
  20. package/dist/A2AServer.js.map +1 -0
  21. package/dist/AgentExecutor.d.ts +89 -0
  22. package/dist/AgentExecutor.d.ts.map +1 -0
  23. package/dist/AgentExecutor.js +126 -0
  24. package/dist/AgentExecutor.js.map +1 -0
  25. package/dist/index.d.ts +12 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +20 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/toAgentCard.d.ts +11 -0
  30. package/dist/toAgentCard.d.ts.map +1 -0
  31. package/dist/toAgentCard.js +19 -0
  32. package/dist/toAgentCard.js.map +1 -0
  33. package/dist/types.d.ts +41 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/types.js +3 -0
  36. package/dist/types.js.map +1 -0
  37. package/package.json +65 -3
  38. package/src/A2AClientEffect.ts +109 -0
  39. package/src/A2AEffect.ts +16 -0
  40. package/src/A2APeerClient.ts +298 -0
  41. package/src/A2AServer.ts +309 -0
  42. package/src/AgentExecutor.ts +192 -0
  43. package/src/index.ts +35 -0
  44. package/src/toAgentCard.ts +31 -0
  45. package/src/types.ts +41 -0
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LoomAgentExecutor = void 0;
4
+ exports.hasActor = hasActor;
5
+ exports.principalNamespace = principalNamespace;
6
+ exports.executionStatusToA2A = executionStatusToA2A;
7
+ exports.toTaskResult = toTaskResult;
8
+ const node_crypto_1 = require("node:crypto");
9
+ /** R12 — does this principal carry the minimum identity a per-caller namespace needs? */
10
+ function hasActor(principal) {
11
+ return typeof principal.actor === "string" && principal.actor.trim().length > 0;
12
+ }
13
+ /**
14
+ * R12 — a stable, opaque key for a principal; the namespace its tasks live in. Two callers with
15
+ * different identity can never address each other's execution ids, because the id the runtime
16
+ * sees embeds this key. Tenant + actor + onBehalfOf are the identity; a delegation token is not
17
+ * (it is a credential, and rotating it must not orphan the caller's tasks).
18
+ */
19
+ function principalNamespace(principal) {
20
+ if (principal === undefined)
21
+ return "anonymous";
22
+ // Fail closed: `PrincipalChain` makes every field optional and allows extras, so a principal
23
+ // identifying callers by another field (`{ sub }`) would hash to ONE shared namespace and hand
24
+ // every such caller each other's tasks. An actor is the minimum identity.
25
+ if (!hasActor(principal)) {
26
+ throw new Error("principalNamespace: the principal has no non-empty string `actor` — it cannot be given a per-caller task namespace");
27
+ }
28
+ const identity = JSON.stringify([
29
+ principal.tenant ?? null,
30
+ principal.actor ?? null,
31
+ principal.onBehalfOf ?? null,
32
+ ]);
33
+ return (0, node_crypto_1.createHash)("sha256").update(identity).digest("hex").slice(0, 24);
34
+ }
35
+ function scopedId(call, clientId, namespaceOf) {
36
+ const ns = call.principal !== undefined ? namespaceOf(call.principal) : (call.namespace ?? "anonymous");
37
+ return `a2a.${ns}.${clientId}`;
38
+ }
39
+ /** Strip the `a2a.<namespace>.` prefix back off an execution id. */
40
+ function clientIdOf(executionId) {
41
+ const m = /^a2a\.[^.]*\.(.*)$/s.exec(executionId);
42
+ return m?.[1] ?? executionId;
43
+ }
44
+ /** Map a Loom execution status onto an A2A task status. */
45
+ function executionStatusToA2A(status) {
46
+ switch (status) {
47
+ case "completed":
48
+ return "completed";
49
+ case "failed":
50
+ return "failed";
51
+ case "running":
52
+ case "suspended":
53
+ return "working";
54
+ default:
55
+ return "submitted";
56
+ }
57
+ }
58
+ /** Fold a terminal/interim ExecutionState into the A2A task result shape. */
59
+ function toTaskResult(taskId, state) {
60
+ const a2aStatus = executionStatusToA2A(state.status);
61
+ const result = { id: taskId, status: a2aStatus };
62
+ if (a2aStatus === "completed") {
63
+ const output = state.variables._output;
64
+ result.artifacts = [
65
+ {
66
+ parts: [
67
+ { type: "text", text: typeof output === "string" ? output : JSON.stringify(output) },
68
+ ],
69
+ },
70
+ ];
71
+ }
72
+ else if (a2aStatus === "failed") {
73
+ result.error = String(state.variables._error ?? "unknown error");
74
+ }
75
+ return result;
76
+ }
77
+ /**
78
+ * The single adapter: drives one Loom flow as an A2A agent. It is STATELESS —
79
+ * `get` rebuilds status from the durable log via `runtime.loadState` rather than
80
+ * holding a live handle, so the server can be horizontally scaled or restarted
81
+ * without losing in-flight tasks (the same statelessness the remote-worker model
82
+ * relies on).
83
+ */
84
+ class LoomAgentExecutor {
85
+ runtime;
86
+ flow;
87
+ options;
88
+ constructor(runtime, flow, options = {}) {
89
+ this.runtime = runtime;
90
+ this.flow = flow;
91
+ this.options = options;
92
+ }
93
+ idFor(call, clientId) {
94
+ return this.options.namespaceByPrincipal === true
95
+ ? scopedId(call, clientId, principalNamespace)
96
+ : clientId;
97
+ }
98
+ async submit(task, call = {}) {
99
+ const prompt = task.message.parts.map((p) => p.text).join("\n");
100
+ const identity = call.principal !== undefined ? { principal: call.principal } : {};
101
+ const handle = await this.runtime.start(this.flow, { prompt }, {
102
+ ...((task.id || this.options.namespaceByPrincipal === true) && {
103
+ executionId: this.idFor(call, task.id || (0, node_crypto_1.randomUUID)()),
104
+ }),
105
+ ...identity,
106
+ });
107
+ // Under namespacing the caller only ever sees the id it chose (or was minted).
108
+ return {
109
+ id: this.options.namespaceByPrincipal === true
110
+ ? clientIdOf(handle.executionId)
111
+ : handle.executionId,
112
+ };
113
+ }
114
+ async get(taskId, call = {}) {
115
+ const identity = call.principal !== undefined ? { principal: call.principal } : {};
116
+ const state = await this.runtime.loadState(this.idFor(call, taskId), identity);
117
+ return state ? toTaskResult(taskId, state) : null;
118
+ }
119
+ async provideInput(taskId, event, call = {}) {
120
+ const identity = call.principal !== undefined ? { principal: call.principal } : {};
121
+ const handle = await this.runtime.resume(this.idFor(call, taskId), event, identity);
122
+ return { id: taskId };
123
+ }
124
+ }
125
+ exports.LoomAgentExecutor = LoomAgentExecutor;
126
+ //# sourceMappingURL=AgentExecutor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AgentExecutor.js","sourceRoot":"","sources":["../src/AgentExecutor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export { A2AEffect } from "./A2AEffect";
2
+ export { createA2AClientEffect } from "./A2AClientEffect";
3
+ export type { A2AClientOptions } from "./A2AClientEffect";
4
+ export { A2APeerClient, fetchAgentCard } from "./A2APeerClient";
5
+ export type { A2APeerConfig, A2APeerInput, A2APeerOutput, A2APeerOutputFor, MappedA2APeer, PeerAgentRegistrar, TaintedA2APeerOutput, } from "./A2APeerClient";
6
+ export { createA2AServer } from "./A2AServer";
7
+ export type { A2AServerOptions, A2AServer } from "./A2AServer";
8
+ export { LoomAgentExecutor, principalNamespace, executionStatusToA2A, toTaskResult, type AgentExecutor, type ExecutorCall, type LoomAgentExecutorOptions, } from "./AgentExecutor";
9
+ export { toAgentCard } from "./toAgentCard";
10
+ export type { ToAgentCardOptions } from "./toAgentCard";
11
+ export type { AgentCard, AgentSkill, A2AMessage, A2AMessagePart, A2ATask, A2ATaskStatus, A2ATaskResult, } from "./types";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAChE,YAAY,EACV,aAAa,EACb,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,wBAAwB,GAC9B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,YAAY,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,cAAc,EACd,OAAO,EACP,aAAa,EACb,aAAa,GACd,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toAgentCard = exports.toTaskResult = exports.executionStatusToA2A = exports.principalNamespace = exports.LoomAgentExecutor = exports.createA2AServer = exports.fetchAgentCard = exports.A2APeerClient = exports.createA2AClientEffect = exports.A2AEffect = void 0;
4
+ var A2AEffect_1 = require("./A2AEffect");
5
+ Object.defineProperty(exports, "A2AEffect", { enumerable: true, get: function () { return A2AEffect_1.A2AEffect; } });
6
+ var A2AClientEffect_1 = require("./A2AClientEffect");
7
+ Object.defineProperty(exports, "createA2AClientEffect", { enumerable: true, get: function () { return A2AClientEffect_1.createA2AClientEffect; } });
8
+ var A2APeerClient_1 = require("./A2APeerClient");
9
+ Object.defineProperty(exports, "A2APeerClient", { enumerable: true, get: function () { return A2APeerClient_1.A2APeerClient; } });
10
+ Object.defineProperty(exports, "fetchAgentCard", { enumerable: true, get: function () { return A2APeerClient_1.fetchAgentCard; } });
11
+ var A2AServer_1 = require("./A2AServer");
12
+ Object.defineProperty(exports, "createA2AServer", { enumerable: true, get: function () { return A2AServer_1.createA2AServer; } });
13
+ var AgentExecutor_1 = require("./AgentExecutor");
14
+ Object.defineProperty(exports, "LoomAgentExecutor", { enumerable: true, get: function () { return AgentExecutor_1.LoomAgentExecutor; } });
15
+ Object.defineProperty(exports, "principalNamespace", { enumerable: true, get: function () { return AgentExecutor_1.principalNamespace; } });
16
+ Object.defineProperty(exports, "executionStatusToA2A", { enumerable: true, get: function () { return AgentExecutor_1.executionStatusToA2A; } });
17
+ Object.defineProperty(exports, "toTaskResult", { enumerable: true, get: function () { return AgentExecutor_1.toTaskResult; } });
18
+ var toAgentCard_1 = require("./toAgentCard");
19
+ Object.defineProperty(exports, "toAgentCard", { enumerable: true, get: function () { return toAgentCard_1.toAgentCard; } });
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,yCAAwC;AAA/B,sGAAA,SAAS,OAAA;AAClB,qDAA0D;AAAjD,wHAAA,qBAAqB,OAAA;AAE9B,iDAAgE;AAAvD,8GAAA,aAAa,OAAA;AAAE,+GAAA,cAAc,OAAA;AAUtC,yCAA8C;AAArC,4GAAA,eAAe,OAAA;AAExB,iDAQyB;AAPvB,kHAAA,iBAAiB,OAAA;AACjB,mHAAA,kBAAkB,OAAA;AAClB,qHAAA,oBAAoB,OAAA;AACpB,6GAAA,YAAY,OAAA;AAKd,6CAA4C;AAAnC,0GAAA,WAAW,OAAA"}
@@ -0,0 +1,11 @@
1
+ import type { AgentDefinition } from "@loomwork/core";
2
+ import type { AgentCard } from "./types";
3
+ export interface ToAgentCardOptions {
4
+ url?: string;
5
+ version?: string;
6
+ capabilities?: {
7
+ streaming?: boolean;
8
+ };
9
+ }
10
+ export declare function toAgentCard(agent: AgentDefinition, opts?: ToAgentCardOptions): AgentCard;
11
+ //# sourceMappingURL=toAgentCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toAgentCard.d.ts","sourceRoot":"","sources":["../src/toAgentCard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAIzC,MAAM,WAAW,kBAAkB;IAIjC,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,YAAY,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACxC;AAOD,wBAAgB,WAAW,CAAC,KAAK,EAAE,eAAe,EAAE,IAAI,GAAE,kBAAuB,GAAG,SAAS,CAS5F"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toAgentCard = toAgentCard;
4
+ // Derive the public A2A AgentCard from an agent definition. name/description come
5
+ // straight from the definition; skills/capabilities/version are read from the
6
+ // (optional) card metadata authored on agent$, falling back to opts then to sane
7
+ // defaults. This is the single source of truth that removes the hand-written,
8
+ // drift-prone card objects.
9
+ function toAgentCard(agent, opts = {}) {
10
+ return {
11
+ name: agent.name,
12
+ description: agent.description ?? "",
13
+ url: opts.url ?? `loom+inproc://${agent.id}`,
14
+ version: agent.version ?? opts.version ?? "1.0.0",
15
+ capabilities: agent.capabilities ?? opts.capabilities ?? { streaming: false },
16
+ skills: agent.skills ?? [],
17
+ };
18
+ }
19
+ //# sourceMappingURL=toAgentCard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toAgentCard.js","sourceRoot":"","sources":["../src/toAgentCard.ts"],"names":[],"mappings":";;AAqBA,kCASC;AAdD,kFAAkF;AAClF,8EAA8E;AAC9E,iFAAiF;AACjF,8EAA8E;AAC9E,4BAA4B;AAC5B,SAAgB,WAAW,CAAC,KAAsB,EAAE,OAA2B,EAAE;IAC/E,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,EAAE;QACpC,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI,iBAAiB,KAAK,CAAC,EAAE,EAAE;QAC5C,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO;QACjD,YAAY,EAAE,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE;QAC7E,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,EAAE;KAC3B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,41 @@
1
+ export interface AgentSkill {
2
+ id: string;
3
+ name: string;
4
+ description?: string;
5
+ inputModes?: string[];
6
+ outputModes?: string[];
7
+ }
8
+ export interface AgentCard {
9
+ name: string;
10
+ description?: string;
11
+ url: string;
12
+ version: string;
13
+ capabilities: {
14
+ streaming?: boolean;
15
+ };
16
+ skills: AgentSkill[];
17
+ }
18
+ export interface A2AMessagePart {
19
+ type: "text";
20
+ text: string;
21
+ }
22
+ export interface A2AMessage {
23
+ role: "user" | "agent";
24
+ parts: A2AMessagePart[];
25
+ }
26
+ export interface A2ATask {
27
+ id: string;
28
+ message: A2AMessage;
29
+ metadata?: Record<string, unknown>;
30
+ }
31
+ export type A2ATaskStatus = "submitted" | "working" | "completed" | "failed";
32
+ export interface A2ATaskResult {
33
+ id: string;
34
+ status: A2ATaskStatus;
35
+ artifacts?: Array<{
36
+ name?: string;
37
+ parts: A2AMessagePart[];
38
+ }>;
39
+ error?: string;
40
+ }
41
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACtC,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,UAAU,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE7E,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,cAAc,EAAE,CAAA;KAAE,CAAC,CAAC;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "@loomwork/plugin-a2a",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Agent-to-Agent (A2A) protocol client and server for Loom workflows",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./cards": {
14
+ "types": "./cards.d.ts",
15
+ "require": "./dist/toAgentCard.js",
16
+ "default": "./dist/toAgentCard.js"
17
+ }
18
+ },
19
+ "dependencies": {
20
+ "zod": "^3.23.8",
21
+ "@loomwork/classify": "^0.1.0",
22
+ "@loomwork/core": "^0.1.0",
23
+ "@loomwork/effect-runtime": "^0.1.0",
24
+ "@loomwork/workflow-runtime": "^0.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.0.0",
28
+ "typescript": "^5.6.3",
29
+ "vitest": "^2.1.8",
30
+ "@loomwork/event-runtime": "0.1.0",
31
+ "@loomwork/snapshot-runtime": "0.1.0",
32
+ "@loomwork/llm": "0.1.0"
33
+ },
34
+ "license": "Apache-2.0",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/chhetrisushil/loom.git",
38
+ "directory": "plugins/a2a"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "src",
43
+ "cards.d.ts",
44
+ "!src/**/*.test.ts",
45
+ "!src/**/*.test.tsx",
46
+ "!src/**/__tests__",
47
+ "!src/**/__fixtures__",
48
+ "!src/**/__snapshots__",
49
+ "!src/**/__golden__",
50
+ "LICENSE",
51
+ "NOTICE"
52
+ ],
53
+ "publishConfig": {
54
+ "access": "public",
55
+ "tag": "next"
56
+ },
57
+ "engines": {
58
+ "node": ">=20.0.0"
59
+ },
60
+ "scripts": {
61
+ "build": "tsc --build",
62
+ "dev": "tsc --build --watch",
63
+ "test": "vitest run",
64
+ "test:watch": "vitest",
65
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
66
+ "typecheck:tests": "tsc -p tsconfig.test.json"
67
+ }
6
68
  }
@@ -0,0 +1,109 @@
1
+ import type { EffectContext } from "@loomwork/effect-runtime";
2
+ import { A2AEffect } from "./A2AEffect";
3
+ import type { A2ATask, A2ATaskResult, A2ATaskStatus } from "./types";
4
+
5
+ export interface A2AClientOptions {
6
+ timeoutMs?: number;
7
+ headers?: Record<string, string>;
8
+ pollIntervalMs?: number;
9
+ }
10
+
11
+ export function createA2AClientEffect(
12
+ agentUrl: string,
13
+ options: A2AClientOptions = {}
14
+ ): A2AEffect<A2ATask, A2ATaskResult> {
15
+ const { timeoutMs = 60_000, pollIntervalMs = 500, headers = {} } = options;
16
+
17
+ return new A2AEffect<A2ATask, A2ATaskResult>(async (task: A2ATask, _ctx: EffectContext) => {
18
+ const baseHeaders = { "content-type": "application/json", ...headers };
19
+
20
+ const sendResp = await fetch(`${agentUrl}/tasks/send`, {
21
+ method: "POST",
22
+ headers: baseHeaders,
23
+ body: JSON.stringify(task),
24
+ });
25
+
26
+ if (!sendResp.ok) {
27
+ throw new Error(`A2A send error ${sendResp.status}: ${await sendResp.text()}`);
28
+ }
29
+
30
+ const sentBody: unknown = await sendResp.json();
31
+ // A malformed answer (not an object at all — e.g. a bare JSON string) used to reach the
32
+ // `"status" in sent` check below and throw a bare, unhelpful `TypeError`. Fail with a
33
+ // message in the style of the other A2A errors instead.
34
+ if (typeof sentBody !== "object" || sentBody === null) {
35
+ throw new Error("A2A send error: malformed answer");
36
+ }
37
+ const sent = sentBody as { id?: unknown; status?: unknown } & Partial<A2ATaskResult>;
38
+ // A peer that answers `/tasks/send` with the finished task (a synchronous agent — e.g. a
39
+ // thin shim whose send handler runs the graph and returns its artifact) has already given
40
+ // the result. Polling for it again cost every such call a full `pollIntervalMs` sleep plus
41
+ // a second request, and failed outright against a peer that does not keep the task (L18).
42
+ // A send that returns only `{ id }` (loom's own server: 202) is polled as before.
43
+ if (typeof sent.status === "string" && terminalStatuses.has(sent.status as A2ATaskStatus)) {
44
+ // A terminal task with no `id` is as malformed as one with no id at all — the caller
45
+ // can't reconcile it against the task it just submitted.
46
+ if (typeof sent.id !== "string") {
47
+ throw new Error("A2A send error: malformed answer");
48
+ }
49
+ return sent as A2ATaskResult;
50
+ }
51
+ if (typeof sent.id !== "string") {
52
+ throw new Error("A2A send error: malformed answer");
53
+ }
54
+ const taskId = sent.id;
55
+ const deadline = Date.now() + timeoutMs;
56
+
57
+ while (Date.now() < deadline) {
58
+ await sleep(pollIntervalMs);
59
+
60
+ const statusResp = await fetch(`${agentUrl}/tasks/${encodeTaskIdSegment(taskId)}`, {
61
+ headers: baseHeaders,
62
+ });
63
+ if (!statusResp.ok) {
64
+ throw new Error(`A2A status error ${statusResp.status}: ${await statusResp.text()}`);
65
+ }
66
+
67
+ const result = (await statusResp.json()) as A2ATaskResult;
68
+
69
+ if (result.status === "completed" || result.status === "failed") {
70
+ return result;
71
+ }
72
+ }
73
+
74
+ throw new Error(`A2A task ${taskId} timed out after ${timeoutMs}ms`);
75
+ });
76
+ }
77
+
78
+ const terminalStatuses = new Set<A2ATaskStatus>(["completed", "failed"]);
79
+
80
+ function sleep(ms: number): Promise<void> {
81
+ return new Promise((resolve) => setTimeout(resolve, ms));
82
+ }
83
+
84
+ /**
85
+ * Escape only the characters that are unsafe in a SINGLE path segment: `/` (would otherwise
86
+ * split the segment — this is what stops a `../admin`-style id from being collapsed by URL
87
+ * dot-segment normalization into a request for a different route), `?` and `#` (would start a
88
+ * query string / fragment), and a literal `%` (would corrupt the escape sequence itself on
89
+ * decode). Everything else — INCLUDING `:` — is left byte-identical to the pre-encoding wire
90
+ * format.
91
+ *
92
+ * `:` matters because every loom task id contains one (`A2APeerClient` sets
93
+ * `task.id = `${executionId}:${stepId}``, plus `:${callKey}` for a decider-scope call, and `TenantScoped` uses `:` as its own separator). A
94
+ * full `encodeURIComponent` here would send `exec%3Astep` — a NEW client talking to an OLD
95
+ * `A2AServer` (or any third-party peer) that does not `decodeURIComponent` the path segment
96
+ * would then 404 every poll during a rolling upgrade. `A2AServer`'s `GET /tasks/:id` route still
97
+ * `decodeURIComponent`s the segment on the way in, which is a no-op for an id with no `%` in it
98
+ * and round-trips one of the escaped characters above, so both old- and new-server peers resolve
99
+ * the same id from the same request.
100
+ *
101
+ * Trade-off accepted deliberately: an id containing a literal `%` cannot be resolved by an old,
102
+ * non-decoding server (it would look up the literal `...%25...` instead of the real id) — but no
103
+ * loom-issued task id contains one.
104
+ */
105
+ function encodeTaskIdSegment(id: string): string {
106
+ return id.replace(/[/?#%]/g, (c) => encodeURIComponent(c));
107
+ }
108
+
109
+ export { terminalStatuses };
@@ -0,0 +1,16 @@
1
+ import type { Effect, EffectContext } from "@loomwork/effect-runtime";
2
+
3
+ /**
4
+ * The effect that fronts a call to an agent-to-agent peer. Its kind, `"a2a"`, names this
5
+ * plugin's protocol; the kernel treats it as an ordinary custom kind (advisory metadata only,
6
+ * ADR 0115). Moved here from `@loomwork/effect-runtime`, which keeps a deprecated copy for one minor.
7
+ */
8
+ export class A2AEffect<TIn = unknown, TOut = unknown> implements Effect<TIn, TOut> {
9
+ readonly kind = "a2a" as const;
10
+
11
+ constructor(private readonly fn: (input: TIn, ctx: EffectContext) => Promise<TOut>) {}
12
+
13
+ execute(input: TIn, ctx: EffectContext): Promise<TOut> {
14
+ return this.fn(input, ctx);
15
+ }
16
+ }