mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 22:37:29 +00:00
feat(agentstack): add @logicsrc/agentstack coordination module
AgentStack is the LogicSRC module for portable agent, task, payment, and reputation coordination across Profullstack apps. Reference implementation of the `agentstack` capability in the Profullstack Shared AppKit OpenSpec. - packages/agentstack: DID helpers (did:coinpay:user/agent), DidTask model + lifecycle, AgentStack in-memory coordinator (agents, tasks, delegation, events), and a validated LogicSRC plugin definition. 9 vitest cases. - Wire @logicsrc/agentstack into the root build chain (after plugin-core). - docs/agentstack.md + mention in openspec-comparison.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c23ce42948
commit
dffeab8430
11 changed files with 620 additions and 2 deletions
79
docs/agentstack.md
Normal file
79
docs/agentstack.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
# AgentStack
|
||||||
|
|
||||||
|
Status: draft module
|
||||||
|
|
||||||
|
AgentStack (`@logicsrc/agentstack`) is the LogicSRC module for **portable agent, task,
|
||||||
|
payment, and reputation coordination** across Profullstack microsaas apps. It is the
|
||||||
|
reference implementation of the `agentstack` capability defined in the Profullstack Shared
|
||||||
|
AppKit OpenSpec (`profullstack-web/openspec/specs/agentstack`).
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Profullstack apps (sh1pt, uGig, qaaas.dev, crawlproof, commandboard.run, coinpayportal,
|
||||||
|
logicsrc, intr0s) need to share agents, tasks, reputation, and payment context without
|
||||||
|
forcing a shared login or one global account database. AgentStack provides a small,
|
||||||
|
provider-neutral coordination layer on top of CoinPay DIDs.
|
||||||
|
|
||||||
|
## Identity model
|
||||||
|
|
||||||
|
```txt
|
||||||
|
User DID: did:coinpay:user:123
|
||||||
|
Agent DID: did:coinpay:agent:abc
|
||||||
|
Task: task_123
|
||||||
|
```
|
||||||
|
|
||||||
|
Each app keeps a local `users.id` and links it to a CoinPay DID. The same person can appear
|
||||||
|
in many apps while every app independently owns its account:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
sh1pt.users.id -> did:coinpay:user:123
|
||||||
|
ugig.users.id -> did:coinpay:user:123
|
||||||
|
qaaas.users.id -> did:coinpay:user:123
|
||||||
|
crawlproof.users.id -> did:coinpay:user:123
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task lifecycle
|
||||||
|
|
||||||
|
```txt
|
||||||
|
pending -> queued -> running -> blocked -> complete | failed | cancelled
|
||||||
|
```
|
||||||
|
|
||||||
|
A task may carry `paymentIntentId`, `escrowId`, and `reputationEventId` so coordination,
|
||||||
|
payment, escrow, and reputation stay linked. Terminal statuses (`complete`, `failed`,
|
||||||
|
`cancelled`) are final.
|
||||||
|
|
||||||
|
## Surface
|
||||||
|
|
||||||
|
- DID helpers: `userDid`, `agentDid`, `makeDid`, `parseDid`
|
||||||
|
- `DidTask`, `AgentProfile`, `DelegationGrant` types
|
||||||
|
- `AgentStack` coordinator: `registerAgent`, `createTask`, `assignTask`,
|
||||||
|
`updateTaskStatus`, `delegate`, `revokeDelegation`, `listTasks`, `snapshot`, `on`
|
||||||
|
- `agentStackPlugin` — a validated LogicSRC `PluginDefinition`
|
||||||
|
|
||||||
|
## Plugin manifest
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"id": "agentstack",
|
||||||
|
"name": "AgentStack",
|
||||||
|
"type": ["agents", "tasks", "coordination"],
|
||||||
|
"capabilities": [
|
||||||
|
"agents.register", "agents.delegate",
|
||||||
|
"tasks.create", "tasks.assign", "tasks.update", "tasks.publish",
|
||||||
|
"reputation.sync", "payments.link", "escrow.link"
|
||||||
|
],
|
||||||
|
"commands": ["agents", "tasks", "delegate"],
|
||||||
|
"env": ["AGENTSTACK_API_URL", "AGENTSTACK_API_KEY", "COINPAY_API_BASE_URL"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relationship to other modules
|
||||||
|
|
||||||
|
- **CoinPay plugin** — provides the DID/wallet/payment/escrow/reputation backend AgentStack
|
||||||
|
references.
|
||||||
|
- **uGig plugin** — publishes tasks to the jobs/gigs marketplace via `tasks.publish`.
|
||||||
|
- **AgentSwarm orchestration** — higher-level multi-agent orchestration can consume
|
||||||
|
AgentStack tasks and delegation grants.
|
||||||
|
|
||||||
|
See also [`openspec-comparison.md`](openspec-comparison.md) and the `agentstack` OpenSpec
|
||||||
|
capability.
|
||||||
|
|
@ -14,7 +14,7 @@ The projects can be complementary. LogicSRC should support an `--openspec` compa
|
||||||
| CLI direction | `logicsrc` as the canonical OpenStandards CLI. | `@fission-ai/openspec` CLI and slash-command integrations with coding tools. |
|
| CLI direction | `logicsrc` as the canonical OpenStandards CLI. | `@fission-ai/openspec` CLI and slash-command integrations with coding tools. |
|
||||||
| MCP | LogicSRC has a standards MCP server and should expose resources, tools, and prompts. | Site states "No MCP" as a product trait. |
|
| MCP | LogicSRC has a standards MCP server and should expose resources, tools, and prompts. | Site states "No MCP" as a product trait. |
|
||||||
| SDK/API | Planned Rust, Bun, Node, Python, curl, and PWA surfaces with matching contracts. | Focus appears to be repo workflow and agent planning artifacts rather than a cross-language SDK/API standard. |
|
| SDK/API | Planned Rust, Bun, Node, Python, curl, and PWA surfaces with matching contracts. | Focus appears to be repo workflow and agent planning artifacts rather than a cross-language SDK/API standard. |
|
||||||
| Plugins | Plugin manifest standard plus CoinPay, uGig, AgentByte, Credential Sharing, and future integration specs. | Integrates with many coding agents and editors; plugin-contract scope is not the main positioning. |
|
| Plugins | Plugin manifest standard plus CoinPay, uGig, AgentStack, AgentByte, Credential Sharing, and future integration specs. | Integrates with many coding agents and editors; plugin-contract scope is not the main positioning. |
|
||||||
| Compatibility idea | `logicsrc --openspec` reads/writes OpenSpec.dev-style specs/proposals/tasks where useful. | Can remain the lightweight planning layer inside repos. |
|
| Compatibility idea | `logicsrc --openspec` reads/writes OpenSpec.dev-style specs/proposals/tasks where useful. | Can remain the lightweight planning layer inside repos. |
|
||||||
|
|
||||||
## CLI Flags
|
## CLI Flags
|
||||||
|
|
|
||||||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -1430,6 +1430,10 @@
|
||||||
"resolved": "packages/account-core",
|
"resolved": "packages/account-core",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@logicsrc/agentstack": {
|
||||||
|
"resolved": "packages/agentstack",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@logicsrc/cli": {
|
"node_modules/@logicsrc/cli": {
|
||||||
"resolved": "packages/cli",
|
"resolved": "packages/cli",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -6063,6 +6067,16 @@
|
||||||
"vitest": "^4.0.8"
|
"vitest": "^4.0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/agentstack": {
|
||||||
|
"name": "@logicsrc/agentstack",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@logicsrc/plugin-core": "file:../../packages/plugin-core"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^4.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@logicsrc/cli",
|
"name": "@logicsrc/cli",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"apps/*"
|
"apps/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm --workspace @logicsrc/schemas run build && npm --workspace @logicsrc/validators run build && npm --workspace @logicsrc/sdk run build && npm --workspace @logicsrc/plugin-core run build && npm --workspace @logicsrc/account-core run build && npm --workspace @logicsrc/plugin-coinpay run build && npm --workspace @logicsrc/plugin-ugig run build && npm --workspace @logicsrc/plugin-sh1pt run build && npm --workspace @logicsrc/plugin-c0mpute run build && npm --workspace @logicsrc/plugin-feed-discovery run build && npm --workspace @logicsrc/plugin-social-accounts run build && npm --workspace @logicsrc/plugin-email-accounts run build && npm --workspace @logicsrc/tui run build && npm --workspace @logicsrc/cli run build && npm --workspace @profullstack/logicsrc-mcp run build && npm --workspace @logicsrc/commandboard-api run build && npm --workspace @logicsrc/commandboard-web run build && npm --workspace @logicsrc/web run build",
|
"build": "npm --workspace @logicsrc/schemas run build && npm --workspace @logicsrc/validators run build && npm --workspace @logicsrc/sdk run build && npm --workspace @logicsrc/plugin-core run build && npm --workspace @logicsrc/agentstack run build && npm --workspace @logicsrc/account-core run build && npm --workspace @logicsrc/plugin-coinpay run build && npm --workspace @logicsrc/plugin-ugig run build && npm --workspace @logicsrc/plugin-sh1pt run build && npm --workspace @logicsrc/plugin-c0mpute run build && npm --workspace @logicsrc/plugin-feed-discovery run build && npm --workspace @logicsrc/plugin-social-accounts run build && npm --workspace @logicsrc/plugin-email-accounts run build && npm --workspace @logicsrc/tui run build && npm --workspace @logicsrc/cli run build && npm --workspace @profullstack/logicsrc-mcp run build && npm --workspace @logicsrc/commandboard-api run build && npm --workspace @logicsrc/commandboard-web run build && npm --workspace @logicsrc/web run build",
|
||||||
"start": "npm --workspace @logicsrc/web run start",
|
"start": "npm --workspace @logicsrc/web run start",
|
||||||
"test": "npm run test --workspaces --if-present",
|
"test": "npm run test --workspaces --if-present",
|
||||||
"check": "npm run build && npm run test",
|
"check": "npm run build && npm run test",
|
||||||
|
|
|
||||||
67
packages/agentstack/README.md
Normal file
67
packages/agentstack/README.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# @logicsrc/agentstack
|
||||||
|
|
||||||
|
AgentStack is the LogicSRC module for **portable agent, task, payment, and reputation
|
||||||
|
coordination** across Profullstack microsaas apps (sh1pt, uGig, qaaas.dev, crawlproof,
|
||||||
|
commandboard.run, coinpayportal, logicsrc, intr0s).
|
||||||
|
|
||||||
|
It is the reference implementation of the `agentstack` capability in the Profullstack
|
||||||
|
Shared AppKit OpenSpec (`profullstack-web/openspec/specs/agentstack`).
|
||||||
|
|
||||||
|
## What it provides
|
||||||
|
|
||||||
|
- **DID helpers** for CoinPay-linked identities: `userDid`, `agentDid`, `makeDid`, `parseDid`
|
||||||
|
(`did:coinpay:user:123`, `did:coinpay:agent:abc`).
|
||||||
|
- **`DidTask`** — a portable task model with a defined status lifecycle
|
||||||
|
(`pending → queued → running → blocked → complete | failed | cancelled`) that can bind to
|
||||||
|
payment, escrow, and reputation events.
|
||||||
|
- **`AgentStack`** — an in-memory coordinator that registers agents, tracks tasks, records
|
||||||
|
delegation grants, and emits coordination events. Storage backends can wrap the same API.
|
||||||
|
- **`agentStackPlugin`** — a LogicSRC `PluginDefinition` (validated against the plugin
|
||||||
|
manifest schema) exposing AgentStack as a coordination plugin with routes, events,
|
||||||
|
permissions, and a TUI panel.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { AgentStack, userDid, agentDid } from "@logicsrc/agentstack";
|
||||||
|
|
||||||
|
const stack = new AgentStack();
|
||||||
|
const owner = userDid("123");
|
||||||
|
|
||||||
|
stack.registerAgent({
|
||||||
|
did: agentDid("abc"),
|
||||||
|
name: "Build Agent",
|
||||||
|
sourceApp: "commandboard.run",
|
||||||
|
supportedProtocols: ["logicsrc/1"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const task = stack.createTask({ ownerDid: owner, sourceApp: "sh1pt.com", title: "Ship build" });
|
||||||
|
stack.assignTask(task.id, agentDid("abc"));
|
||||||
|
stack.updateTaskStatus(task.id, "running");
|
||||||
|
stack.updateTaskStatus(task.id, "complete", { reputationEventId: "rep_1" });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cross-app identity
|
||||||
|
|
||||||
|
Each app keeps a local `users.id` and links it to a CoinPay DID, giving cross-app identity
|
||||||
|
without shared cookies or one global account database:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
sh1pt.users.id -> did:coinpay:user:123
|
||||||
|
ugig.users.id -> did:coinpay:user:123
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
```txt
|
||||||
|
AGENTSTACK_API_URL
|
||||||
|
AGENTSTACK_API_KEY
|
||||||
|
COINPAY_API_BASE_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # tsc -> dist
|
||||||
|
npm test # vitest
|
||||||
|
```
|
||||||
18
packages/agentstack/package.json
Normal file
18
packages/agentstack/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "@logicsrc/agentstack",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "AgentStack: portable agent, task, payment, and reputation coordination module for LogicSRC.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"test": "vitest run src --passWithNoTests"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@logicsrc/plugin-core": "file:../../packages/plugin-core"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^4.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
106
packages/agentstack/src/index.test.ts
Normal file
106
packages/agentstack/src/index.test.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { validateManifest } from "@logicsrc/plugin-core";
|
||||||
|
import {
|
||||||
|
AgentStack,
|
||||||
|
agentDid,
|
||||||
|
agentStackManifest,
|
||||||
|
agentStackPlugin,
|
||||||
|
makeDid,
|
||||||
|
parseDid,
|
||||||
|
userDid
|
||||||
|
} from "./index.js";
|
||||||
|
import type { AgentProfile } from "./types.js";
|
||||||
|
|
||||||
|
const owner = userDid("123");
|
||||||
|
const agent: AgentProfile = {
|
||||||
|
did: agentDid("abc"),
|
||||||
|
name: "Build Agent",
|
||||||
|
sourceApp: "commandboard.run",
|
||||||
|
supportedProtocols: ["logicsrc/1"]
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("DID helpers", () => {
|
||||||
|
it("builds and parses CoinPay DIDs", () => {
|
||||||
|
expect(makeDid("user", "123")).toBe("did:coinpay:user:123");
|
||||||
|
expect(parseDid(owner)).toEqual({ kind: "user", id: "123" });
|
||||||
|
expect(parseDid(agentDid("abc"))).toEqual({ kind: "agent", id: "abc" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects non-CoinPay DIDs", () => {
|
||||||
|
expect(parseDid("did:web:example.com")).toBeNull();
|
||||||
|
expect(parseDid("did:coinpay:bot:abc")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("AgentStack manifest", () => {
|
||||||
|
it("is a valid LogicSRC plugin manifest", () => {
|
||||||
|
expect(() => validateManifest(agentStackManifest)).not.toThrow();
|
||||||
|
expect(agentStackPlugin.manifest.id).toBe("agentstack");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("AgentStack coordinator", () => {
|
||||||
|
it("creates pending tasks and queues assigned ones", () => {
|
||||||
|
const stack = new AgentStack();
|
||||||
|
const pending = stack.createTask({ ownerDid: owner, sourceApp: "ugig.net", title: "Crawl site" });
|
||||||
|
expect(pending.status).toBe("pending");
|
||||||
|
|
||||||
|
stack.registerAgent(agent);
|
||||||
|
const assigned = stack.createTask({
|
||||||
|
ownerDid: owner,
|
||||||
|
sourceApp: "ugig.net",
|
||||||
|
title: "Crawl site 2",
|
||||||
|
assigneeDid: agent.did
|
||||||
|
});
|
||||||
|
expect(assigned.status).toBe("queued");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves a task through its lifecycle and binds reputation", () => {
|
||||||
|
const stack = new AgentStack();
|
||||||
|
stack.registerAgent(agent);
|
||||||
|
const task = stack.createTask({ ownerDid: owner, sourceApp: "sh1pt.com", title: "Ship build" });
|
||||||
|
|
||||||
|
stack.assignTask(task.id, agent.did);
|
||||||
|
stack.updateTaskStatus(task.id, "running");
|
||||||
|
const done = stack.updateTaskStatus(task.id, "complete", { reputationEventId: "rep_1" });
|
||||||
|
|
||||||
|
expect(done.status).toBe("complete");
|
||||||
|
expect(done.assigneeDid).toBe(agent.did);
|
||||||
|
expect(done.reputationEventId).toBe("rep_1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses to transition out of a terminal status", () => {
|
||||||
|
const stack = new AgentStack();
|
||||||
|
const task = stack.createTask({ ownerDid: owner, sourceApp: "qaaas.dev", title: "Test run" });
|
||||||
|
stack.updateTaskStatus(task.id, "cancelled");
|
||||||
|
expect(() => stack.updateTaskStatus(task.id, "running")).toThrow(/cancelled/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records delegation grants and emits events", () => {
|
||||||
|
const stack = new AgentStack();
|
||||||
|
const listener = vi.fn();
|
||||||
|
stack.on(listener);
|
||||||
|
stack.registerAgent(agent);
|
||||||
|
const grant = stack.delegate(owner, agent.did, ["tasks:create"]);
|
||||||
|
|
||||||
|
expect(grant.ownerDid).toBe(owner);
|
||||||
|
expect(grant.agentDid).toBe(agent.did);
|
||||||
|
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: "agent.registered" }));
|
||||||
|
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: "delegation.granted" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unknown agents and invalid DIDs", () => {
|
||||||
|
const stack = new AgentStack();
|
||||||
|
expect(() => stack.createTask({ ownerDid: "nope", sourceApp: "x", title: "t" })).toThrow();
|
||||||
|
const task = stack.createTask({ ownerDid: owner, sourceApp: "x", title: "t" });
|
||||||
|
expect(() => stack.assignTask(task.id, agentDid("ghost"))).toThrow(/Unknown agent/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters tasks in snapshots", () => {
|
||||||
|
const stack = new AgentStack();
|
||||||
|
stack.createTask({ ownerDid: owner, sourceApp: "x", title: "a" });
|
||||||
|
stack.createTask({ ownerDid: userDid("999"), sourceApp: "x", title: "b" });
|
||||||
|
expect(stack.listTasks({ ownerDid: owner })).toHaveLength(1);
|
||||||
|
expect(stack.snapshot().tasks).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
218
packages/agentstack/src/index.ts
Normal file
218
packages/agentstack/src/index.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
import type { PluginDefinition } from "@logicsrc/plugin-core";
|
||||||
|
import { agentStackManifest } from "./manifest.js";
|
||||||
|
import type {
|
||||||
|
AgentProfile,
|
||||||
|
AgentStackEvent,
|
||||||
|
AgentStackListener,
|
||||||
|
AgentStackSnapshot,
|
||||||
|
CreateTaskInput,
|
||||||
|
DelegationGrant,
|
||||||
|
DidKind,
|
||||||
|
DidTask,
|
||||||
|
TaskStatus
|
||||||
|
} from "./types.js";
|
||||||
|
import { DID_METHOD } from "./types.js";
|
||||||
|
|
||||||
|
/** Build a CoinPay-method DID for a user or agent: `did:coinpay:user:123`. */
|
||||||
|
export function makeDid(kind: DidKind, id: string): string {
|
||||||
|
return `${DID_METHOD}:${kind}:${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userDid = (id: string) => makeDid("user", id);
|
||||||
|
export const agentDid = (id: string) => makeDid("agent", id);
|
||||||
|
|
||||||
|
/** Parse a CoinPay DID into its kind and id, or return null if it is not one. */
|
||||||
|
export function parseDid(did: string): { kind: DidKind; id: string } | null {
|
||||||
|
const prefix = `${DID_METHOD}:`;
|
||||||
|
if (!did.startsWith(prefix)) return null;
|
||||||
|
const [kind, id] = did.slice(prefix.length).split(":");
|
||||||
|
if ((kind !== "user" && kind !== "agent") || !id) return null;
|
||||||
|
return { kind, id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDidTask(value: unknown): value is DidTask {
|
||||||
|
return (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
typeof (value as DidTask).id === "string" &&
|
||||||
|
typeof (value as DidTask).ownerDid === "string" &&
|
||||||
|
typeof (value as DidTask).status === "string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TERMINAL: ReadonlySet<TaskStatus> = new Set(["complete", "failed", "cancelled"]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory AgentStack coordinator: registers agents, tracks portable tasks through their
|
||||||
|
* lifecycle, records delegation grants, and emits coordination events. Reference
|
||||||
|
* implementation of the `agentstack` capability; storage backends can wrap the same API.
|
||||||
|
*/
|
||||||
|
export class AgentStack {
|
||||||
|
private readonly agents = new Map<string, AgentProfile>();
|
||||||
|
private readonly tasks = new Map<string, DidTask>();
|
||||||
|
private readonly delegations = new Map<string, DelegationGrant>();
|
||||||
|
private readonly listeners = new Set<AgentStackListener>();
|
||||||
|
private seq = 0;
|
||||||
|
|
||||||
|
constructor(private readonly now: () => string = () => new Date().toISOString()) {}
|
||||||
|
|
||||||
|
on(listener: AgentStackListener): () => void {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
return () => this.listeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit(event: AgentStackEvent) {
|
||||||
|
for (const listener of this.listeners) listener(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextId(prefix: string): string {
|
||||||
|
this.seq += 1;
|
||||||
|
return `${prefix}_${this.seq}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerAgent(agent: AgentProfile): AgentProfile {
|
||||||
|
if (!parseDid(agent.did)) {
|
||||||
|
throw new Error(`Invalid agent DID: ${agent.did}`);
|
||||||
|
}
|
||||||
|
this.agents.set(agent.did, agent);
|
||||||
|
this.emit({ type: "agent.registered", agent });
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAgent(did: string): AgentProfile | undefined {
|
||||||
|
return this.agents.get(did);
|
||||||
|
}
|
||||||
|
|
||||||
|
createTask(input: CreateTaskInput): DidTask {
|
||||||
|
if (!parseDid(input.ownerDid)) {
|
||||||
|
throw new Error(`Invalid owner DID: ${input.ownerDid}`);
|
||||||
|
}
|
||||||
|
const ts = this.now();
|
||||||
|
const task: DidTask = {
|
||||||
|
id: this.nextId("task"),
|
||||||
|
ownerDid: input.ownerDid,
|
||||||
|
assigneeDid: input.assigneeDid,
|
||||||
|
sourceApp: input.sourceApp,
|
||||||
|
title: input.title,
|
||||||
|
description: input.description,
|
||||||
|
status: input.assigneeDid ? "queued" : "pending",
|
||||||
|
paymentIntentId: input.paymentIntentId,
|
||||||
|
escrowId: input.escrowId,
|
||||||
|
metadata: input.metadata,
|
||||||
|
createdAt: ts,
|
||||||
|
updatedAt: ts
|
||||||
|
};
|
||||||
|
this.tasks.set(task.id, task);
|
||||||
|
this.emit({ type: "task.created", task });
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTask(id: string): DidTask | undefined {
|
||||||
|
return this.tasks.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
assignTask(taskId: string, agentDidValue: string): DidTask {
|
||||||
|
const task = this.requireTask(taskId);
|
||||||
|
if (!this.agents.has(agentDidValue)) {
|
||||||
|
throw new Error(`Unknown agent: ${agentDidValue}`);
|
||||||
|
}
|
||||||
|
const updated: DidTask = {
|
||||||
|
...task,
|
||||||
|
assigneeDid: agentDidValue,
|
||||||
|
status: task.status === "pending" ? "queued" : task.status,
|
||||||
|
updatedAt: this.now()
|
||||||
|
};
|
||||||
|
this.tasks.set(taskId, updated);
|
||||||
|
this.emit({ type: "task.assigned", task: updated });
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTaskStatus(
|
||||||
|
taskId: string,
|
||||||
|
status: TaskStatus,
|
||||||
|
patch: Partial<Pick<DidTask, "reputationEventId" | "paymentIntentId" | "escrowId" | "metadata">> = {}
|
||||||
|
): DidTask {
|
||||||
|
const task = this.requireTask(taskId);
|
||||||
|
if (TERMINAL.has(task.status)) {
|
||||||
|
throw new Error(`Task ${taskId} is already ${task.status} and cannot transition to ${status}`);
|
||||||
|
}
|
||||||
|
const updated: DidTask = { ...task, ...patch, status, updatedAt: this.now() };
|
||||||
|
this.tasks.set(taskId, updated);
|
||||||
|
this.emit({ type: "task.updated", task: updated });
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Grant an agent authority to act for an owner. */
|
||||||
|
delegate(ownerDidValue: string, agentDidValue: string, scopes: string[], expiresAt?: string): DelegationGrant {
|
||||||
|
if (!parseDid(ownerDidValue)) throw new Error(`Invalid owner DID: ${ownerDidValue}`);
|
||||||
|
if (!this.agents.has(agentDidValue)) throw new Error(`Unknown agent: ${agentDidValue}`);
|
||||||
|
const grant: DelegationGrant = {
|
||||||
|
id: this.nextId("grant"),
|
||||||
|
ownerDid: ownerDidValue,
|
||||||
|
agentDid: agentDidValue,
|
||||||
|
scopes,
|
||||||
|
expiresAt,
|
||||||
|
createdAt: this.now()
|
||||||
|
};
|
||||||
|
this.delegations.set(grant.id, grant);
|
||||||
|
this.emit({ type: "delegation.granted", grant });
|
||||||
|
return grant;
|
||||||
|
}
|
||||||
|
|
||||||
|
revokeDelegation(grantId: string): DelegationGrant {
|
||||||
|
const grant = this.delegations.get(grantId);
|
||||||
|
if (!grant) throw new Error(`Unknown delegation grant: ${grantId}`);
|
||||||
|
this.delegations.delete(grantId);
|
||||||
|
this.emit({ type: "delegation.revoked", grant });
|
||||||
|
return grant;
|
||||||
|
}
|
||||||
|
|
||||||
|
listTasks(filter?: { ownerDid?: string; assigneeDid?: string; status?: TaskStatus }): DidTask[] {
|
||||||
|
return [...this.tasks.values()].filter((task) => {
|
||||||
|
if (filter?.ownerDid && task.ownerDid !== filter.ownerDid) return false;
|
||||||
|
if (filter?.assigneeDid && task.assigneeDid !== filter.assigneeDid) return false;
|
||||||
|
if (filter?.status && task.status !== filter.status) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot(): AgentStackSnapshot {
|
||||||
|
return {
|
||||||
|
agents: [...this.agents.values()],
|
||||||
|
tasks: [...this.tasks.values()],
|
||||||
|
delegations: [...this.delegations.values()]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private requireTask(id: string): DidTask {
|
||||||
|
const task = this.tasks.get(id);
|
||||||
|
if (!task) throw new Error(`Unknown task: ${id}`);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** LogicSRC plugin definition exposing AgentStack as a coordination plugin. */
|
||||||
|
export const agentStackPlugin: PluginDefinition = {
|
||||||
|
manifest: agentStackManifest,
|
||||||
|
configDefaults: {
|
||||||
|
enabled: false,
|
||||||
|
api_url: "${AGENTSTACK_API_URL}",
|
||||||
|
api_key: "${AGENTSTACK_API_KEY}",
|
||||||
|
coinpay_api_base_url: "${COINPAY_API_BASE_URL}"
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{ method: "POST", path: "/api/plugins/agentstack/agents", capability: "agents.register" },
|
||||||
|
{ method: "POST", path: "/api/plugins/agentstack/tasks", capability: "tasks.create" },
|
||||||
|
{ method: "PATCH", path: "/api/plugins/agentstack/tasks/:id", capability: "tasks.update" },
|
||||||
|
{ method: "POST", path: "/api/plugins/agentstack/delegations", capability: "agents.delegate" }
|
||||||
|
],
|
||||||
|
events: [
|
||||||
|
{ event: "task.created", capability: "tasks.publish" },
|
||||||
|
{ event: "task.approved", capability: "reputation.sync" }
|
||||||
|
],
|
||||||
|
permissions: ["agents:register", "agents:delegate", "tasks:create", "tasks:update", "reputation:sync"],
|
||||||
|
tuiPanels: [{ id: "agentstack-tasks", title: "AgentStack Tasks" }]
|
||||||
|
};
|
||||||
|
|
||||||
|
export { agentStackManifest };
|
||||||
|
export * from "./types.js";
|
||||||
22
packages/agentstack/src/manifest.ts
Normal file
22
packages/agentstack/src/manifest.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import type { PluginManifest } from "@logicsrc/plugin-core";
|
||||||
|
|
||||||
|
export const agentStackManifest: PluginManifest = {
|
||||||
|
id: "agentstack",
|
||||||
|
name: "AgentStack",
|
||||||
|
version: "0.1.0",
|
||||||
|
type: ["agents", "tasks", "coordination"],
|
||||||
|
default: false,
|
||||||
|
capabilities: [
|
||||||
|
"agents.register",
|
||||||
|
"agents.delegate",
|
||||||
|
"tasks.create",
|
||||||
|
"tasks.assign",
|
||||||
|
"tasks.update",
|
||||||
|
"tasks.publish",
|
||||||
|
"reputation.sync",
|
||||||
|
"payments.link",
|
||||||
|
"escrow.link"
|
||||||
|
],
|
||||||
|
commands: ["agents", "tasks", "delegate"],
|
||||||
|
env: ["AGENTSTACK_API_URL", "AGENTSTACK_API_KEY", "COINPAY_API_BASE_URL"]
|
||||||
|
};
|
||||||
86
packages/agentstack/src/types.ts
Normal file
86
packages/agentstack/src/types.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* AgentStack types — portable agent, task, payment, and reputation coordination.
|
||||||
|
*
|
||||||
|
* Mirrors the `agentstack` capability spec in the Profullstack Shared AppKit OpenSpec
|
||||||
|
* (profullstack-web/openspec/specs/agentstack).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type TaskStatus =
|
||||||
|
| "pending"
|
||||||
|
| "queued"
|
||||||
|
| "running"
|
||||||
|
| "blocked"
|
||||||
|
| "complete"
|
||||||
|
| "failed"
|
||||||
|
| "cancelled";
|
||||||
|
|
||||||
|
/** DID method shared across CoinPay-linked Profullstack apps. */
|
||||||
|
export const DID_METHOD = "did:coinpay";
|
||||||
|
|
||||||
|
export type DidKind = "user" | "agent";
|
||||||
|
|
||||||
|
/** A portable, DID-addressable task that can move across apps. */
|
||||||
|
export interface DidTask {
|
||||||
|
id: string;
|
||||||
|
ownerDid: string;
|
||||||
|
assigneeDid?: string;
|
||||||
|
sourceApp: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
status: TaskStatus;
|
||||||
|
paymentIntentId?: string;
|
||||||
|
escrowId?: string;
|
||||||
|
reputationEventId?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A registered agent identity that can be delegated tasks. */
|
||||||
|
export interface AgentProfile {
|
||||||
|
did: string;
|
||||||
|
name: string;
|
||||||
|
sourceApp: string;
|
||||||
|
inboxUrl?: string;
|
||||||
|
taskEndpoint?: string;
|
||||||
|
supportedProtocols: string[];
|
||||||
|
reputationScore?: number;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A grant authorizing an agent to act on an owner's behalf. */
|
||||||
|
export interface DelegationGrant {
|
||||||
|
id: string;
|
||||||
|
ownerDid: string;
|
||||||
|
agentDid: string;
|
||||||
|
scopes: string[];
|
||||||
|
expiresAt?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AgentStackEvent =
|
||||||
|
| { type: "agent.registered"; agent: AgentProfile }
|
||||||
|
| { type: "task.created"; task: DidTask }
|
||||||
|
| { type: "task.assigned"; task: DidTask }
|
||||||
|
| { type: "task.updated"; task: DidTask }
|
||||||
|
| { type: "delegation.granted"; grant: DelegationGrant }
|
||||||
|
| { type: "delegation.revoked"; grant: DelegationGrant };
|
||||||
|
|
||||||
|
export type AgentStackListener = (event: AgentStackEvent) => void;
|
||||||
|
|
||||||
|
export interface CreateTaskInput {
|
||||||
|
ownerDid: string;
|
||||||
|
sourceApp: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
assigneeDid?: string;
|
||||||
|
paymentIntentId?: string;
|
||||||
|
escrowId?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentStackSnapshot {
|
||||||
|
agents: AgentProfile[];
|
||||||
|
tasks: DidTask[];
|
||||||
|
delegations: DelegationGrant[];
|
||||||
|
}
|
||||||
8
packages/agentstack/tsconfig.json
Normal file
8
packages/agentstack/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue