mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 14:37:26 +00:00
fix(agentstack): assignTask refuses tasks in a terminal status (#47)
This commit is contained in:
parent
a4cc229120
commit
90fffb124e
2 changed files with 332 additions and 309 deletions
|
|
@ -1,74 +1,74 @@
|
|||
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");
|
||||
});
|
||||
|
||||
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" });
|
||||
|
|
@ -76,31 +76,51 @@ describe("AgentStack coordinator", () => {
|
|||
expect(() => stack.updateTaskStatus(task.id, "running")).toThrow(/cancelled/);
|
||||
});
|
||||
|
||||
it("refuses to assign a task that is already in a terminal status", () => {
|
||||
const stack = new AgentStack();
|
||||
stack.registerAgent(agent);
|
||||
const other: AgentProfile = { ...agent, did: agentDid("xyz") };
|
||||
stack.registerAgent(other);
|
||||
const task = stack.createTask({
|
||||
ownerDid: owner,
|
||||
sourceApp: "ugig.net",
|
||||
title: "Paid task",
|
||||
paymentIntentId: "pi_1",
|
||||
escrowId: "esc_1"
|
||||
});
|
||||
stack.assignTask(task.id, agent.did);
|
||||
stack.updateTaskStatus(task.id, "complete", { reputationEventId: "rep_1" });
|
||||
|
||||
expect(() => stack.assignTask(task.id, other.did)).toThrow(/already complete and cannot be assigned/);
|
||||
expect(stack.getTask(task.id)?.assigneeDid).toBe(agent.did);
|
||||
expect(stack.getTask(task.id)?.status).toBe("complete");
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,218 +1,221 @@
|
|||
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);
|
||||
}
|
||||
|
||||
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 (TERMINAL.has(task.status)) {
|
||||
throw new Error(`Task ${taskId} is already ${task.status} and cannot be assigned`);
|
||||
}
|
||||
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";
|
||||
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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue