@runravel/ravel 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 (74) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +202 -0
  3. package/README.md +68 -0
  4. package/bin/ravel.mjs +6 -0
  5. package/examples/acme/agent.md +14 -0
  6. package/examples/acme/growth/agent.md +19 -0
  7. package/examples/acme/growth/copywriter/agent.md +13 -0
  8. package/examples/acme/growth/copywriter/tools.json +11 -0
  9. package/examples/acme/growth/researcher/agent.md +11 -0
  10. package/examples/acme/processes/prospect-outreach.process.md +24 -0
  11. package/examples/harbor/agent.md +31 -0
  12. package/examples/harbor/processes/new-client-quote.process.md +31 -0
  13. package/examples/harbor/processes/resolve-ticket-batch.process.md +33 -0
  14. package/examples/harbor/sales/agent.md +29 -0
  15. package/examples/harbor/sales/sdr/agent.md +24 -0
  16. package/examples/harbor/sales/solutions/agent.md +29 -0
  17. package/examples/harbor/sales/solutions/tools.json +16 -0
  18. package/examples/harbor/support/agent.md +31 -0
  19. package/examples/harbor/support/kb-writer/agent.md +25 -0
  20. package/examples/harbor/support/kb-writer/tools.json +10 -0
  21. package/examples/harbor/support/qa-reviewer/agent.md +23 -0
  22. package/examples/harbor/support/tools.json +16 -0
  23. package/examples/harbor/support/triage/agent.md +24 -0
  24. package/examples/harbor/support/triage/tools.json +10 -0
  25. package/examples/plugin-demo/agent.md +15 -0
  26. package/examples/plugin-demo/processes/jot.process.md +21 -0
  27. package/examples/plugin-demo/scribe/agent.md +15 -0
  28. package/examples/plugin-demo/scribe/plugin.ts +53 -0
  29. package/examples/plugin-demo/scribe/tools.json +9 -0
  30. package/package.json +65 -0
  31. package/src/cli/main.ts +428 -0
  32. package/src/control-plane/registry.ts +294 -0
  33. package/src/control-plane/watcher.ts +132 -0
  34. package/src/domain/ids.ts +17 -0
  35. package/src/domain/pricing.ts +51 -0
  36. package/src/domain/types.ts +168 -0
  37. package/src/index.ts +35 -0
  38. package/src/memory/genericTools.ts +276 -0
  39. package/src/memory/kv.ts +52 -0
  40. package/src/memory/store.ts +76 -0
  41. package/src/messaging/bus.ts +143 -0
  42. package/src/messaging/inbox.ts +119 -0
  43. package/src/orchestrator/orchestrator.ts +270 -0
  44. package/src/orchestrator/planner.ts +218 -0
  45. package/src/platform/app.ts +287 -0
  46. package/src/plugins/loader.ts +86 -0
  47. package/src/plugins/server.ts +41 -0
  48. package/src/plugins/types.ts +96 -0
  49. package/src/runtime/agent.ts +488 -0
  50. package/src/runtime/engine.ts +84 -0
  51. package/src/runtime/fakeEngine.ts +75 -0
  52. package/src/runtime/lifecycle.ts +85 -0
  53. package/src/runtime/officeActions.ts +58 -0
  54. package/src/runtime/officeTools.ts +42 -0
  55. package/src/runtime/sdkEngine.ts +213 -0
  56. package/src/schemas/agent.ts +47 -0
  57. package/src/schemas/common.ts +52 -0
  58. package/src/schemas/frontmatter.ts +36 -0
  59. package/src/schemas/process.ts +55 -0
  60. package/src/schemas/tools.ts +83 -0
  61. package/src/secrets/store.ts +131 -0
  62. package/src/service/scheduler.ts +377 -0
  63. package/src/service/server.ts +554 -0
  64. package/src/trust/approval.ts +180 -0
  65. package/src/trust/audit.ts +195 -0
  66. package/src/trust/budget.ts +64 -0
  67. package/src/trust/emittingAudit.ts +32 -0
  68. package/src/trust/executor.ts +73 -0
  69. package/src/trust/killswitch.ts +73 -0
  70. package/src/trust/observability.ts +97 -0
  71. package/src/trust/proposals.ts +114 -0
  72. package/ui/dist/assets/index-C6CxDaPS.js +44 -0
  73. package/ui/dist/assets/index-CD-lhs0Z.css +1 -0
  74. package/ui/dist/index.html +13 -0
@@ -0,0 +1,97 @@
1
+ import { addUsage, emptyUsage, type Usage } from "../domain/types.js";
2
+ import type { AuditSink, AuditEvent } from "./audit.js";
3
+ import type { ProposalStore } from "./proposals.js";
4
+ import type { Lifecycle } from "../runtime/lifecycle.js";
5
+ import type { AgentState, AgentActivity } from "../runtime/agent.js";
6
+
7
+ export interface AgentMetric {
8
+ nodeId: string;
9
+ name: string;
10
+ role: string;
11
+ state: AgentState;
12
+ tasksRun: number;
13
+ usage: Usage;
14
+ /** What the agent is doing right now (empty when idle). */
15
+ activity: AgentActivity;
16
+ }
17
+
18
+ export interface ProcessRunMetric {
19
+ runId: string;
20
+ process: string;
21
+ status: string;
22
+ turns: number;
23
+ usage: Usage;
24
+ }
25
+
26
+ export interface DashboardSnapshot {
27
+ totalUsage: Usage;
28
+ agents: AgentMetric[];
29
+ processRuns: ProcessRunMetric[];
30
+ /** Deferred actions awaiting an async human decision. */
31
+ pendingProposals: number;
32
+ deadLetters: number;
33
+ eventCount: number;
34
+ }
35
+
36
+ /**
37
+ * Derives a live cost/trace view from the append-only audit trail plus the
38
+ * current lifecycle/approval state. Observability is first-class: the owner can
39
+ * always see what each agent has spent, what's running, and what's waiting on
40
+ * them — no separate instrumentation, just a read over the audit log.
41
+ */
42
+ export class Observer {
43
+ constructor(
44
+ private readonly audit: AuditSink,
45
+ private readonly lifecycle: Lifecycle,
46
+ private readonly proposals: ProposalStore,
47
+ private readonly deadLetterCount: () => number = () => 0,
48
+ ) {}
49
+
50
+ snapshot(): DashboardSnapshot {
51
+ const events = this.audit.all();
52
+ const perNodeUsage = new Map<string, Usage>();
53
+ const perNodeTasks = new Map<string, number>();
54
+ const processRuns: ProcessRunMetric[] = [];
55
+ let totalUsage = emptyUsage();
56
+
57
+ for (const e of events) {
58
+ if (e.type === "task.finished" && e.nodeId !== undefined) {
59
+ const u = (e.data["usage"] as Usage | undefined) ?? emptyUsage();
60
+ perNodeUsage.set(e.nodeId, addUsage(perNodeUsage.get(e.nodeId) ?? emptyUsage(), u));
61
+ perNodeTasks.set(e.nodeId, (perNodeTasks.get(e.nodeId) ?? 0) + 1);
62
+ totalUsage = addUsage(totalUsage, u);
63
+ } else if (e.type === "process.finished") {
64
+ processRuns.push(this.toProcessMetric(e));
65
+ }
66
+ }
67
+
68
+ const agents: AgentMetric[] = this.lifecycle.all().map((rt) => ({
69
+ nodeId: rt.node.id,
70
+ name: rt.node.spec.name,
71
+ role: rt.node.spec.role ?? rt.node.spec.name,
72
+ state: rt.state,
73
+ tasksRun: perNodeTasks.get(rt.node.id) ?? 0,
74
+ usage: perNodeUsage.get(rt.node.id) ?? emptyUsage(),
75
+ activity: rt.activity,
76
+ }));
77
+
78
+ return {
79
+ totalUsage,
80
+ agents,
81
+ processRuns,
82
+ pendingProposals: this.proposals.pending().length,
83
+ deadLetters: this.deadLetterCount(),
84
+ eventCount: events.length,
85
+ };
86
+ }
87
+
88
+ private toProcessMetric(e: AuditEvent): ProcessRunMetric {
89
+ return {
90
+ runId: e.runId ?? "",
91
+ process: String(e.data["process"] ?? ""),
92
+ status: String(e.data["status"] ?? ""),
93
+ turns: Number(e.data["turns"] ?? 0),
94
+ usage: (e.data["usage"] as Usage | undefined) ?? emptyUsage(),
95
+ };
96
+ }
97
+ }
@@ -0,0 +1,114 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { EventEmitter } from "node:events";
5
+ import { newId, systemClock, type Clock } from "../domain/ids.js";
6
+ import type { Proposal, ProposalStatus } from "../domain/types.js";
7
+
8
+ /** Stable hash of a tool input, for de-duping identical pending proposals. */
9
+ function hashInput(input: unknown): string {
10
+ const stable = JSON.stringify(input, (_k, v) =>
11
+ v && typeof v === "object" && !Array.isArray(v)
12
+ ? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)))
13
+ : v,
14
+ );
15
+ return createHash("sha256").update(stable ?? "null").digest("hex").slice(0, 16);
16
+ }
17
+
18
+ function dedupeKey(p: Pick<Proposal, "runId" | "nodeId" | "action" | "input">): string {
19
+ return `${p.runId ?? ""}|${p.nodeId}|${p.action}|${hashInput(p.input)}`;
20
+ }
21
+
22
+ /**
23
+ * Durable store of proposed consequential actions awaiting human approval.
24
+ *
25
+ * - Source of truth is the in-memory map; the JSON file is a serialized mirror
26
+ * rewritten on each mutation (volumes are small — a flood is a design smell
27
+ * the approval queue is meant to surface, not absorb).
28
+ * - `create` de-dupes against existing *pending* proposals on
29
+ * (runId, nodeId, action, inputHash), so a stray task re-dispatch can't queue
30
+ * the same action twice.
31
+ * - Emits `created` / `updated` so the dashboard and the SSE event stream stay live.
32
+ */
33
+ export class ProposalStore extends EventEmitter {
34
+ private readonly byId = new Map<string, Proposal>();
35
+ private writeChain: Promise<void> = Promise.resolve();
36
+
37
+ constructor(
38
+ private readonly opts: { filePath?: string; clock?: Clock } = {},
39
+ ) {
40
+ super();
41
+ }
42
+
43
+ private get clock(): Clock {
44
+ return this.opts.clock ?? systemClock;
45
+ }
46
+
47
+ /** Load persisted proposals (call once at startup). */
48
+ async load(): Promise<void> {
49
+ if (!this.opts.filePath) return;
50
+ try {
51
+ const raw = await fs.readFile(this.opts.filePath, "utf8");
52
+ for (const p of JSON.parse(raw) as Proposal[]) this.byId.set(p.id, p);
53
+ } catch (err) {
54
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Record a proposed action. Returns the existing pending proposal if an
60
+ * identical one is already queued (idempotent), else a new one.
61
+ */
62
+ async create(fields: Omit<Proposal, "id" | "createdAt" | "status">): Promise<Proposal> {
63
+ const key = dedupeKey(fields);
64
+ for (const existing of this.byId.values()) {
65
+ if (existing.status === "pending" && dedupeKey(existing) === key) return existing;
66
+ }
67
+ const proposal: Proposal = {
68
+ ...fields,
69
+ id: newId("prop"),
70
+ createdAt: this.clock.iso(),
71
+ status: "pending",
72
+ };
73
+ this.byId.set(proposal.id, proposal);
74
+ await this.persist();
75
+ this.emit("created", proposal);
76
+ return proposal;
77
+ }
78
+
79
+ get(id: string): Proposal | undefined {
80
+ return this.byId.get(id);
81
+ }
82
+
83
+ list(status?: ProposalStatus): Proposal[] {
84
+ const all = [...this.byId.values()];
85
+ return status ? all.filter((p) => p.status === status) : all;
86
+ }
87
+
88
+ pending(): Proposal[] {
89
+ return this.list("pending");
90
+ }
91
+
92
+ /** Update a proposal's status (and optional result/error). No-op if unknown. */
93
+ async setStatus(id: string, status: ProposalStatus, patch: Partial<Proposal> = {}): Promise<Proposal | null> {
94
+ const current = this.byId.get(id);
95
+ if (!current) return null;
96
+ const updated: Proposal = { ...current, ...patch, status };
97
+ if (status === "approved" || status === "rejected") updated.decidedAt = this.clock.iso();
98
+ if (status === "executed" || status === "failed") updated.executedAt = this.clock.iso();
99
+ this.byId.set(id, updated);
100
+ await this.persist();
101
+ this.emit("updated", updated);
102
+ return updated;
103
+ }
104
+
105
+ private async persist(): Promise<void> {
106
+ if (!this.opts.filePath) return;
107
+ const snapshot = JSON.stringify([...this.byId.values()]);
108
+ this.writeChain = this.writeChain.then(async () => {
109
+ await fs.mkdir(path.dirname(this.opts.filePath!), { recursive: true });
110
+ await fs.writeFile(this.opts.filePath!, snapshot, "utf8");
111
+ });
112
+ await this.writeChain;
113
+ }
114
+ }