@stackbone/cli 0.1.0-alpha.9 → 0.2.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.
- package/CHANGELOG.md +201 -1
- package/README.md +22 -19
- package/main.js +23560 -10010
- package/migrations/0001_runs.sql +1 -1
- package/migrations/0002_approvals.sql +1 -1
- package/migrations/0003_runs_is_playground_index.sql +1 -1
- package/migrations/0005_stackbone_viewer.sql +1 -1
- package/migrations/0006_secrets.sql +1 -1
- package/migrations/0007_agent_config_versions.sql +1 -1
- package/migrations/0014_rag.sql +1 -1
- package/migrations/0015_queue_jobs_lease.sql +60 -0
- package/migrations/0016_runs_job_trigger.sql +15 -0
- package/migrations/0017_runs_workflow_chat_triggers.sql +25 -0
- package/migrations/0018_run_steps_v2.sql +167 -0
- package/migrations/0019_sessions.sql +106 -0
- package/migrations/0020_runs_entity_name.sql +23 -0
- package/migrations/0021_trigger_subscriptions.sql +67 -0
- package/package.json +3 -2
- package/runtime-deps/@stackbone/workflow-world/index.js +8913 -0
- package/stackbone-cli-0.2.0.tgz +0 -0
- package/stackbone-cli-0.1.0-alpha.9.tgz +0 -0
package/migrations/0001_runs.sql
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
-- 0001_runs.sql — schema and tables for the run-level surface of Studio.
|
|
2
2
|
-- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6 (runs, run_steps).
|
|
3
|
-
-- ADR: docs/arquitectura/decisiones/2026-05-
|
|
3
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md §D2/D7/D9.
|
|
4
4
|
|
|
5
5
|
CREATE SCHEMA IF NOT EXISTS stackbone_platform;
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
-- 0002_approvals.sql — HITL approvals queue and audit trail.
|
|
2
2
|
-- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6.6/§6.7.
|
|
3
3
|
-- Component: docs/arquitectura/componentes/08-hitl-inbox.md.
|
|
4
|
-
-- ADR: docs/arquitectura/decisiones/2026-05-
|
|
4
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md §D2.
|
|
5
5
|
|
|
6
6
|
CREATE TABLE IF NOT EXISTS stackbone_platform.approvals (
|
|
7
7
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
-- 0003_runs_is_playground_index.sql — billing-friendly partial index over runs.
|
|
2
2
|
-- Spec: docs/tasks/Studio V5 - Playground e Invoke.md (V5#01).
|
|
3
|
-
-- ADR: docs/arquitectura/decisiones/2026-05-
|
|
3
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md §D5/D9.
|
|
4
4
|
--
|
|
5
5
|
-- The `is_playground` column itself already lives in 0001_runs.sql (F2 baked it
|
|
6
6
|
-- in defensively so playground writes had a place to land before V5 shipped).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
-- 0005_stackbone_viewer.sql — read-only role for the Studio DB Explorer.
|
|
2
2
|
-- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"DB Explorer".
|
|
3
3
|
-- Task: docs/tasks/Studio V6 - DB Explorer read-only.md (V6#01).
|
|
4
|
-
-- ADR: docs/arquitectura/decisiones/2026-05-
|
|
4
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md §D6.
|
|
5
5
|
--
|
|
6
6
|
-- The role is `NOLOGIN` because callers (apps/api, apps/cli emulator) reach it
|
|
7
7
|
-- via `SET ROLE stackbone_viewer` on a session opened with the regular owner
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
-- `stackbone dev` emulator (Studio V10).
|
|
3
3
|
-- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"Secrets".
|
|
4
4
|
-- Task: docs/tasks/Studio V10 - Secrets.md (V10#02).
|
|
5
|
-
-- ADR: docs/arquitectura/decisiones/2026-05-
|
|
5
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md
|
|
6
6
|
-- (decision V10#01 in the task itself: cloud secrets stay in
|
|
7
7
|
-- `installation_secret` of the control plane; local emulator stores
|
|
8
8
|
-- the same wire shape here so the Studio UI sees identical data.)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
-- payloads for Studio's Config dinámica screen (V11).
|
|
3
3
|
-- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"Mapa de pantallas / C.Config dinámica".
|
|
4
4
|
-- Task: docs/tasks/Studio V11 - Config dinámica.md (V11#01).
|
|
5
|
-
-- ADR: docs/arquitectura/decisiones/2026-05-
|
|
5
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-30-stackbone-studio.md §D2/D7.
|
|
6
6
|
--
|
|
7
7
|
-- The current config is the row with the highest `version`. Older rows are
|
|
8
8
|
-- kept for the rollback action and the lateral history list. Hot-reload is
|
package/migrations/0014_rag.sql
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
-- 0014_rag.sql — the canonical RAG store (collections / documents / chunks /
|
|
2
2
|
-- jobs), moved into the platform schema.
|
|
3
|
-
-- ADR: docs/adr/2026-06-05-rag
|
|
3
|
+
-- ADR: docs/adr/2026-06-05-rag.md
|
|
4
4
|
--
|
|
5
5
|
-- Previously these four tables lived in `public` and were installed by the
|
|
6
6
|
-- creator's own migrator (`stackbone db migrate add-rag`). They are a
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
-- 0015_queue_jobs_lease.sql — lease columns + a terminal NOTIFY on the agent's
|
|
2
|
+
-- `_queue_jobs` journal (P1 "async jobs").
|
|
3
|
+
--
|
|
4
|
+
-- The wrapper around a dispatched handler now leases the in-flight row so the
|
|
5
|
+
-- control-plane reaper can tell a still-running job from a crashed one:
|
|
6
|
+
--
|
|
7
|
+
-- heartbeat_at — touched by the harness roughly every 15s while a dispatched
|
|
8
|
+
-- handler is running. A stale heartbeat is what lets the
|
|
9
|
+
-- reaper DERIVE the `lost` state (never written as a column —
|
|
10
|
+
-- the status CHECK stays `done` / `failed`).
|
|
11
|
+
-- deadline_at — started_at + the effective handler timeout; the hard ceiling
|
|
12
|
+
-- past which the reaper treats the lease as expired.
|
|
13
|
+
-- run_id — correlation key with the `runs` timeline so Studio can join
|
|
14
|
+
-- a journal row to its run.
|
|
15
|
+
--
|
|
16
|
+
-- All three are nullable and added IF NOT EXISTS: the install saga may re-apply
|
|
17
|
+
-- migrations on a redeploy, and a job journaled before this migration simply
|
|
18
|
+
-- carries NULL leases.
|
|
19
|
+
--
|
|
20
|
+
-- A terminal NOTIFY fans out the NULL→terminal transition on
|
|
21
|
+
-- `stackbone_platform_events` so the dispatcher's tracker can resolve a job the
|
|
22
|
+
-- instant its journal row flips, instead of polling. It mirrors the trigger
|
|
23
|
+
-- style of `0001_runs.sql`.
|
|
24
|
+
|
|
25
|
+
ALTER TABLE stackbone_platform._queue_jobs
|
|
26
|
+
ADD COLUMN IF NOT EXISTS heartbeat_at timestamptz;
|
|
27
|
+
ALTER TABLE stackbone_platform._queue_jobs
|
|
28
|
+
ADD COLUMN IF NOT EXISTS deadline_at timestamptz;
|
|
29
|
+
ALTER TABLE stackbone_platform._queue_jobs
|
|
30
|
+
ADD COLUMN IF NOT EXISTS run_id text;
|
|
31
|
+
|
|
32
|
+
-- AFTER UPDATE trigger fans out into pg_notify('stackbone_platform_events', ...)
|
|
33
|
+
-- ONLY on the NULL→terminal status transition (the job just finished). The
|
|
34
|
+
-- dispatcher's tracker LISTENs on this channel and resolves the in-flight job
|
|
35
|
+
-- the moment its journal row flips to `done` / `failed`.
|
|
36
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.notify_queue_job_event() RETURNS trigger
|
|
37
|
+
LANGUAGE plpgsql AS $fn$
|
|
38
|
+
DECLARE
|
|
39
|
+
payload jsonb;
|
|
40
|
+
BEGIN
|
|
41
|
+
IF (OLD.status IS NULL) AND (NEW.status IS NOT NULL) THEN
|
|
42
|
+
payload := jsonb_build_object(
|
|
43
|
+
'type', 'queue_job.finished',
|
|
44
|
+
'dispatch_id', NEW.dispatch_id,
|
|
45
|
+
'run_id', NEW.run_id,
|
|
46
|
+
'status', NEW.status
|
|
47
|
+
);
|
|
48
|
+
ELSE
|
|
49
|
+
RETURN NULL;
|
|
50
|
+
END IF;
|
|
51
|
+
|
|
52
|
+
PERFORM pg_notify('stackbone_platform_events', payload::text);
|
|
53
|
+
RETURN NULL;
|
|
54
|
+
END;
|
|
55
|
+
$fn$;
|
|
56
|
+
|
|
57
|
+
DROP TRIGGER IF EXISTS queue_jobs_notify_au ON stackbone_platform._queue_jobs;
|
|
58
|
+
CREATE TRIGGER queue_jobs_notify_au
|
|
59
|
+
AFTER UPDATE ON stackbone_platform._queue_jobs
|
|
60
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_queue_job_event();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- 0016_runs_job_trigger.sql — allow `trigger = 'job'` on stackbone_platform.runs.
|
|
2
|
+
--
|
|
3
|
+
-- Dispatched queue jobs (ADR 2026-05-30) are first-class runs, but until now a
|
|
4
|
+
-- queue dispatch only wrote the `_queue_jobs` journal — it never inserted a
|
|
5
|
+
-- `runs` row. Two consequences: the dispatched run was INVISIBLE in the Studio
|
|
6
|
+
-- runs timeline, and `ctx.progress` / `ctx.step` inside a job handler hit the
|
|
7
|
+
-- `run_steps.run_id REFERENCES runs(id)` FK with no parent row, so the progress
|
|
8
|
+
-- note was silently dropped. The wrapper now materialises a `trigger='job'` run
|
|
9
|
+
-- row when it journals the job's started lifecycle, which fixes both — but the
|
|
10
|
+
-- original `0001_runs.sql` CHECK predates the `job` trigger and would reject the
|
|
11
|
+
-- insert. Widen it to match `runTriggerSchema` (which already carries `job`).
|
|
12
|
+
ALTER TABLE stackbone_platform.runs DROP CONSTRAINT IF EXISTS runs_trigger_check;
|
|
13
|
+
ALTER TABLE stackbone_platform.runs
|
|
14
|
+
ADD CONSTRAINT runs_trigger_check
|
|
15
|
+
CHECK (trigger IN ('manual', 'webhook', 'cron', 'event', 'job'));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- 0017_runs_workflow_chat_triggers.sql — widen the runs/run_steps CHECKs for
|
|
2
|
+
-- the eve durable runtime (feature 67).
|
|
3
|
+
--
|
|
4
|
+
-- The Workflow World → runs projection (F5) materialises each durable workflow
|
|
5
|
+
-- as a first-class run so it shows up in the Studio timeline; it tags those rows
|
|
6
|
+
-- with `trigger='workflow'` and records each workflow step as a generic
|
|
7
|
+
-- `run_steps.type='workflow_step'`. The chat recorder (F6) persists every eve
|
|
8
|
+
-- chat turn as a run tagged `trigger='chat'` (the chat session id rides the
|
|
9
|
+
-- free-form `runs.metadata` column — no new column is added). The earlier
|
|
10
|
+
-- CHECKs (0001 listed `manual/webhook/cron/event`, 0016 added `job`) predate all
|
|
11
|
+
-- three values and would reject the inserts. Widen `runs_trigger_check` to match
|
|
12
|
+
-- `runTriggerSchema` and `run_steps_type_check` to match the SDK `RunStepType`.
|
|
13
|
+
ALTER TABLE stackbone_platform.runs DROP CONSTRAINT IF EXISTS runs_trigger_check;
|
|
14
|
+
ALTER TABLE stackbone_platform.runs
|
|
15
|
+
ADD CONSTRAINT runs_trigger_check
|
|
16
|
+
CHECK (trigger IN ('manual', 'webhook', 'cron', 'event', 'job', 'workflow', 'chat'));
|
|
17
|
+
|
|
18
|
+
ALTER TABLE stackbone_platform.run_steps DROP CONSTRAINT IF EXISTS run_steps_type_check;
|
|
19
|
+
ALTER TABLE stackbone_platform.run_steps
|
|
20
|
+
ADD CONSTRAINT run_steps_type_check
|
|
21
|
+
CHECK (type IN (
|
|
22
|
+
'agent', 'llm_call', 'db_query', 'http_fetch',
|
|
23
|
+
'queue_publish', 'hitl_pause', 'tool_call',
|
|
24
|
+
'rag_query', 'storage_op', 'workflow_step'
|
|
25
|
+
));
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
-- 0018_run_steps_v2.sql — demolish the legacy run_steps surface and rebuild it
|
|
2
|
+
-- around the eve + Workflow World step telemetry (feature 68, Fase 2 / A4).
|
|
3
|
+
--
|
|
4
|
+
-- The old run_steps table (0001) modelled hand-written SDK steps (`ctx.step`
|
|
5
|
+
-- with types like `llm_call`/`db_query`/…), carried no token columns and was
|
|
6
|
+
-- driven by the now-removed span processor / `ctx.step` writers. Feature 67 only
|
|
7
|
+
-- widened its type CHECK to admit `workflow_step`; feature 68 drops the whole
|
|
8
|
+
-- table and recreates it v2.
|
|
9
|
+
--
|
|
10
|
+
-- v2 projects the durable Workflow World step timeline (one row per World step,
|
|
11
|
+
-- `type='workflow_step'`) plus, for the fast-follow, per-model-call rows
|
|
12
|
+
-- (`type='model_call'`). Each row carries the four eve token counters
|
|
13
|
+
-- (input/output/cache-read/cache-write); the total is derived at read time, not
|
|
14
|
+
-- stored. `world_step_id` is the World correlation/idempotency key the projector
|
|
15
|
+
-- upserts on. No cost column (D5 — tokens only, no $).
|
|
16
|
+
--
|
|
17
|
+
-- This migration also drops `runs.cost_estimated_usd` (the demolished cost
|
|
18
|
+
-- accumulator) and recreates the `approvals.requested_by_step_id` FK against
|
|
19
|
+
-- run_steps v2 (A6), plus the `notify_run_step_event` pg_notify trigger that the
|
|
20
|
+
-- table drop took down with it.
|
|
21
|
+
|
|
22
|
+
-- --------------------------------------------------------------------------
|
|
23
|
+
-- Demolition (A3): drop the legacy table, the cost column and the dependent FK.
|
|
24
|
+
-- --------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
-- The FK lives on `approvals` and references the old run_steps; drop it before
|
|
27
|
+
-- the table so the table drop does not have to CASCADE through it. Recreated
|
|
28
|
+
-- against v2 at the end of this migration.
|
|
29
|
+
ALTER TABLE stackbone_platform.approvals
|
|
30
|
+
DROP CONSTRAINT IF EXISTS approvals_requested_by_step_id_fkey;
|
|
31
|
+
ALTER TABLE stackbone_platform.approvals
|
|
32
|
+
DROP COLUMN IF EXISTS requested_by_step_id;
|
|
33
|
+
|
|
34
|
+
-- The AFTER INSERT/UPDATE trigger and its function are owned by the old table;
|
|
35
|
+
-- drop both so the recreate below is unambiguous.
|
|
36
|
+
DROP TRIGGER IF EXISTS run_steps_notify_aiu ON stackbone_platform.run_steps;
|
|
37
|
+
DROP FUNCTION IF EXISTS stackbone_platform.notify_run_step_event();
|
|
38
|
+
|
|
39
|
+
-- Self-referential parent FK and run FK go away with the table. CASCADE clears
|
|
40
|
+
-- the dependent self-FK in one shot.
|
|
41
|
+
DROP TABLE IF EXISTS stackbone_platform.run_steps CASCADE;
|
|
42
|
+
|
|
43
|
+
-- The per-run cost accumulator is demolished (no $ cost anywhere in this
|
|
44
|
+
-- feature).
|
|
45
|
+
ALTER TABLE stackbone_platform.runs
|
|
46
|
+
DROP COLUMN IF EXISTS cost_estimated_usd;
|
|
47
|
+
|
|
48
|
+
-- Per-turn token counters on `runs` (v1, A5). An eve agent CHAT turn = one
|
|
49
|
+
-- `runs` row (trigger='chat'); the chat recorder stamps the four eve
|
|
50
|
+
-- `data.usage` counters onto that row (the per-model-call granularity stays in
|
|
51
|
+
-- run_steps, used by workflows). All nullable; the total is `input + output`,
|
|
52
|
+
-- derived at read, never stored (D4). The runs timeline and feature 69's
|
|
53
|
+
-- session aggregate read these directly off `runs`.
|
|
54
|
+
ALTER TABLE stackbone_platform.runs
|
|
55
|
+
ADD COLUMN IF NOT EXISTS input_tokens integer,
|
|
56
|
+
ADD COLUMN IF NOT EXISTS output_tokens integer,
|
|
57
|
+
ADD COLUMN IF NOT EXISTS cache_read_tokens integer,
|
|
58
|
+
ADD COLUMN IF NOT EXISTS cache_write_tokens integer;
|
|
59
|
+
|
|
60
|
+
-- --------------------------------------------------------------------------
|
|
61
|
+
-- Recreate run_steps v2.
|
|
62
|
+
-- --------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
CREATE TABLE stackbone_platform.run_steps (
|
|
65
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
66
|
+
run_id uuid NOT NULL
|
|
67
|
+
REFERENCES stackbone_platform.runs(id) ON DELETE CASCADE,
|
|
68
|
+
-- Nullable self-FK. In practice null for flat "use step" workflows (the
|
|
69
|
+
-- World fork does not populate step-parent provenance for them), but kept
|
|
70
|
+
-- for the nested-step tree the projector derives from the event-log writer.
|
|
71
|
+
parent_step_id uuid REFERENCES stackbone_platform.run_steps(id) ON DELETE CASCADE,
|
|
72
|
+
type text NOT NULL
|
|
73
|
+
CHECK (type IN ('workflow_step', 'model_call')),
|
|
74
|
+
name text,
|
|
75
|
+
status text NOT NULL DEFAULT 'running'
|
|
76
|
+
CHECK (status IN (
|
|
77
|
+
'pending', 'running', 'completed',
|
|
78
|
+
'failed', 'cancelled'
|
|
79
|
+
)),
|
|
80
|
+
started_at timestamptz,
|
|
81
|
+
completed_at timestamptz,
|
|
82
|
+
duration_ms integer,
|
|
83
|
+
input jsonb,
|
|
84
|
+
output jsonb,
|
|
85
|
+
error jsonb,
|
|
86
|
+
-- The four eve token counters (data.usage on `step.completed`). All
|
|
87
|
+
-- nullable: workflow steps carry none (the World has no tokens) and any
|
|
88
|
+
-- counter may be absent. The total is `input + output`, derived at read
|
|
89
|
+
-- time — never persisted (D4).
|
|
90
|
+
input_tokens integer,
|
|
91
|
+
output_tokens integer,
|
|
92
|
+
cache_read_tokens integer,
|
|
93
|
+
cache_write_tokens integer,
|
|
94
|
+
-- World correlation / idempotency key (= the World `stepId`). The
|
|
95
|
+
-- event-driven projector upserts on this so a replayed/retried step updates
|
|
96
|
+
-- the same row instead of inserting a duplicate.
|
|
97
|
+
world_step_id text,
|
|
98
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE INDEX IF NOT EXISTS run_steps_run_started_idx
|
|
102
|
+
ON stackbone_platform.run_steps (run_id, started_at);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS run_steps_parent_idx
|
|
104
|
+
ON stackbone_platform.run_steps (parent_step_id);
|
|
105
|
+
|
|
106
|
+
-- Idempotent upsert target for the projector: one row per (run, World step).
|
|
107
|
+
-- Partial so rows without a World correlation (e.g. future synthetic rows)
|
|
108
|
+
-- never block one another on a NULL key.
|
|
109
|
+
CREATE UNIQUE INDEX IF NOT EXISTS run_steps_world_step_uq
|
|
110
|
+
ON stackbone_platform.run_steps (run_id, world_step_id)
|
|
111
|
+
WHERE world_step_id IS NOT NULL;
|
|
112
|
+
|
|
113
|
+
-- --------------------------------------------------------------------------
|
|
114
|
+
-- Recreate the pg_notify trigger on the same `stackbone_platform_events`
|
|
115
|
+
-- channel the SSE handler / emulator bus already LISTEN on. `created` fires on
|
|
116
|
+
-- INSERT; `finished` fires when completed_at transitions from null to a value.
|
|
117
|
+
-- --------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.notify_run_step_event() RETURNS trigger
|
|
120
|
+
LANGUAGE plpgsql AS $fn$
|
|
121
|
+
DECLARE
|
|
122
|
+
payload jsonb;
|
|
123
|
+
BEGIN
|
|
124
|
+
IF TG_OP = 'INSERT' THEN
|
|
125
|
+
payload := jsonb_build_object(
|
|
126
|
+
'type', 'run.step.created',
|
|
127
|
+
'run_id', NEW.run_id,
|
|
128
|
+
'step_id', NEW.id,
|
|
129
|
+
'parent_step_id', NEW.parent_step_id,
|
|
130
|
+
'step_type', NEW.type,
|
|
131
|
+
'started_at', NEW.started_at
|
|
132
|
+
);
|
|
133
|
+
ELSIF TG_OP = 'UPDATE' THEN
|
|
134
|
+
IF (OLD.completed_at IS NULL) AND (NEW.completed_at IS NOT NULL) THEN
|
|
135
|
+
payload := jsonb_build_object(
|
|
136
|
+
'type', 'run.step.finished',
|
|
137
|
+
'run_id', NEW.run_id,
|
|
138
|
+
'step_id', NEW.id,
|
|
139
|
+
'status', NEW.status,
|
|
140
|
+
'duration_ms', NEW.duration_ms
|
|
141
|
+
);
|
|
142
|
+
ELSE
|
|
143
|
+
RETURN NULL;
|
|
144
|
+
END IF;
|
|
145
|
+
ELSE
|
|
146
|
+
RETURN NULL;
|
|
147
|
+
END IF;
|
|
148
|
+
|
|
149
|
+
PERFORM pg_notify('stackbone_platform_events', payload::text);
|
|
150
|
+
RETURN NULL;
|
|
151
|
+
END;
|
|
152
|
+
$fn$;
|
|
153
|
+
|
|
154
|
+
DROP TRIGGER IF EXISTS run_steps_notify_aiu ON stackbone_platform.run_steps;
|
|
155
|
+
CREATE TRIGGER run_steps_notify_aiu
|
|
156
|
+
AFTER INSERT OR UPDATE ON stackbone_platform.run_steps
|
|
157
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_run_step_event();
|
|
158
|
+
|
|
159
|
+
-- --------------------------------------------------------------------------
|
|
160
|
+
-- Recreate the approvals → run_steps FK against v2 (A6). `requestApproval`
|
|
161
|
+
-- stamps the id of the workflow step that asked for the approval so the inbox
|
|
162
|
+
-- can anchor it to the exact step.
|
|
163
|
+
-- --------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
ALTER TABLE stackbone_platform.approvals
|
|
166
|
+
ADD COLUMN IF NOT EXISTS requested_by_step_id uuid
|
|
167
|
+
REFERENCES stackbone_platform.run_steps(id) ON DELETE SET NULL;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
-- 0019_sessions.sql — project eve agent conversations as a first-class entity
|
|
2
|
+
-- (feature 69, Fase 1).
|
|
3
|
+
--
|
|
4
|
+
-- An eve agent conversation is durable in the agent's Workflow World (one
|
|
5
|
+
-- long-lived `workflowEntry` run per session). The control plane never read the
|
|
6
|
+
-- World back, so a conversation had no row/endpoint/screen — only the loose
|
|
7
|
+
-- `runs.metadata.sessionId` jsonb the chat proxy stamped on each turn. This
|
|
8
|
+
-- migration adds the SESSION as a projected entity: a lean `sessions` table fed
|
|
9
|
+
-- by the World→sessions projector (Fase 2), plus a real `runs.session_id` column
|
|
10
|
+
-- so a session's turns join by an indexed key instead of the jsonb.
|
|
11
|
+
--
|
|
12
|
+
-- Lean by design (D6): `sessions` stores ONLY lifecycle (status + timestamps).
|
|
13
|
+
-- The read-time aggregates (turn_count, last_activity_at, preview, tokens) are a
|
|
14
|
+
-- GROUP BY over `runs` keyed on `session_id`; nothing here couples to feature
|
|
15
|
+
-- 68's token columns.
|
|
16
|
+
|
|
17
|
+
-- --------------------------------------------------------------------------
|
|
18
|
+
-- The sessions table. `id` is the deterministic UUIDv5 the projector derives
|
|
19
|
+
-- from the eve sessionId (= the `workflowEntry` World runId) under its own
|
|
20
|
+
-- namespace — always caller-provided, so no gen_random_uuid default. The
|
|
21
|
+
-- `status` CHECK is intentionally EXTENSIBLE (D5): v1 models only the three
|
|
22
|
+
-- lifecycle states; the later HITL-in-chat feature widens it with
|
|
23
|
+
-- `awaiting_input` via a non-breaking ALTER (the 0017 pattern), not a rewrite.
|
|
24
|
+
-- --------------------------------------------------------------------------
|
|
25
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.sessions (
|
|
26
|
+
id uuid PRIMARY KEY,
|
|
27
|
+
-- The eve agent this conversation targets. Nullable in local (the emulator
|
|
28
|
+
-- is single-tenant), required in cloud — mirrors `runs.agent_id`.
|
|
29
|
+
agent_id text,
|
|
30
|
+
status text NOT NULL DEFAULT 'open'
|
|
31
|
+
CHECK (status IN ('open', 'completed', 'failed')),
|
|
32
|
+
started_at timestamptz,
|
|
33
|
+
ended_at timestamptz,
|
|
34
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
-- Cursor pagination over (created_at, id), the same keyset shape the runs list
|
|
38
|
+
-- uses, so the Studio Sessions list reuses one decoder.
|
|
39
|
+
CREATE INDEX IF NOT EXISTS sessions_created_at_id_idx
|
|
40
|
+
ON stackbone_platform.sessions (created_at DESC, id DESC);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS sessions_agent_status_idx
|
|
43
|
+
ON stackbone_platform.sessions (agent_id, status, created_at DESC);
|
|
44
|
+
|
|
45
|
+
-- --------------------------------------------------------------------------
|
|
46
|
+
-- `runs.session_id` — the real join key (D4). Stamped by the chat recorder and
|
|
47
|
+
-- the projector with the SAME deterministic UUIDv5 as `sessions.id`. A LOGICAL
|
|
48
|
+
-- FK only (NOT enforced): a turn can persist before the session row exists
|
|
49
|
+
-- (eventual consistency; the projector and recorder race), so a real FK would
|
|
50
|
+
-- reject the turn. `runs.metadata.sessionId` stays for back-compat but the join
|
|
51
|
+
-- moves to this column. Indexed for the `WHERE session_id = :id` turn lookup.
|
|
52
|
+
-- --------------------------------------------------------------------------
|
|
53
|
+
ALTER TABLE stackbone_platform.runs
|
|
54
|
+
ADD COLUMN IF NOT EXISTS session_id uuid;
|
|
55
|
+
|
|
56
|
+
CREATE INDEX IF NOT EXISTS runs_session_id_idx
|
|
57
|
+
ON stackbone_platform.runs (session_id, created_at)
|
|
58
|
+
WHERE session_id IS NOT NULL;
|
|
59
|
+
|
|
60
|
+
-- --------------------------------------------------------------------------
|
|
61
|
+
-- pg_notify trigger on the SAME `stackbone_platform_events` channel the
|
|
62
|
+
-- runs/run_steps/approvals triggers use, so the cloud `StudioStreamService` (one
|
|
63
|
+
-- LISTEN) and the emulator's run-steps-bridge forward `session.*` with no new
|
|
64
|
+
-- infra. `created` fires on INSERT; `updated` fires when the lifecycle actually
|
|
65
|
+
-- moved (status or ended_at changed) so an idempotent re-upsert of the same
|
|
66
|
+
-- state does not spam the stream.
|
|
67
|
+
-- --------------------------------------------------------------------------
|
|
68
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.notify_session_event() RETURNS trigger
|
|
69
|
+
LANGUAGE plpgsql AS $fn$
|
|
70
|
+
DECLARE
|
|
71
|
+
payload jsonb;
|
|
72
|
+
BEGIN
|
|
73
|
+
IF TG_OP = 'INSERT' THEN
|
|
74
|
+
payload := jsonb_build_object(
|
|
75
|
+
'type', 'session.created',
|
|
76
|
+
'session_id', NEW.id,
|
|
77
|
+
'agent_id', NEW.agent_id,
|
|
78
|
+
'status', NEW.status,
|
|
79
|
+
'started_at', NEW.started_at,
|
|
80
|
+
'created_at', NEW.created_at
|
|
81
|
+
);
|
|
82
|
+
ELSIF TG_OP = 'UPDATE' THEN
|
|
83
|
+
IF (OLD.status IS DISTINCT FROM NEW.status)
|
|
84
|
+
OR (OLD.ended_at IS DISTINCT FROM NEW.ended_at) THEN
|
|
85
|
+
payload := jsonb_build_object(
|
|
86
|
+
'type', 'session.updated',
|
|
87
|
+
'session_id', NEW.id,
|
|
88
|
+
'status', NEW.status,
|
|
89
|
+
'ended_at', NEW.ended_at
|
|
90
|
+
);
|
|
91
|
+
ELSE
|
|
92
|
+
RETURN NULL;
|
|
93
|
+
END IF;
|
|
94
|
+
ELSE
|
|
95
|
+
RETURN NULL;
|
|
96
|
+
END IF;
|
|
97
|
+
|
|
98
|
+
PERFORM pg_notify('stackbone_platform_events', payload::text);
|
|
99
|
+
RETURN NULL;
|
|
100
|
+
END;
|
|
101
|
+
$fn$;
|
|
102
|
+
|
|
103
|
+
DROP TRIGGER IF EXISTS sessions_notify_aiu ON stackbone_platform.sessions;
|
|
104
|
+
CREATE TRIGGER sessions_notify_aiu
|
|
105
|
+
AFTER INSERT OR UPDATE ON stackbone_platform.sessions
|
|
106
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_session_event();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- 0020_runs_entity_name.sql — record which agent/workflow a run belongs to.
|
|
2
|
+
--
|
|
3
|
+
-- The Studio Runs screen lists a run's id, trigger and status but never the
|
|
4
|
+
-- NAME of the agent or workflow that produced it, so a workspace running several
|
|
5
|
+
-- eve agents + workflows could not tell whose run a row was, nor filter by it.
|
|
6
|
+
-- The name is known at run-creation time (the chat proxy has the agent `:name`;
|
|
7
|
+
-- the workflow start has the workflow `:name`) but was dropped.
|
|
8
|
+
--
|
|
9
|
+
-- `entity_name` denormalises that name onto the run so the list renders it as a
|
|
10
|
+
-- column and filters by it without a join. It is the agent name for `chat` runs
|
|
11
|
+
-- and the workflow name for `workflow` runs; combined with the existing
|
|
12
|
+
-- `trigger` the UI knows the kind. Nullable: classic/manual cloud runs (eve is
|
|
13
|
+
-- dev-only) leave it NULL. Distinct from `agent_id`, which in cloud is the agent
|
|
14
|
+
-- TEMPLATE id (a stable identifier, not a display name) and is null for workflows.
|
|
15
|
+
|
|
16
|
+
ALTER TABLE stackbone_platform.runs
|
|
17
|
+
ADD COLUMN IF NOT EXISTS entity_name text;
|
|
18
|
+
|
|
19
|
+
-- Backs both the exact-match filter (`WHERE entity_name = $1`) and the distinct
|
|
20
|
+
-- list that populates the Runs screen's name dropdown.
|
|
21
|
+
CREATE INDEX IF NOT EXISTS runs_entity_name_idx
|
|
22
|
+
ON stackbone_platform.runs (entity_name)
|
|
23
|
+
WHERE entity_name IS NOT NULL;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
-- 0021_trigger_subscriptions.sql — the inbound side of Stackbone Connect
|
|
2
|
+
-- (feature 89): an operator links a connector polling TRIGGER to a World
|
|
3
|
+
-- workflow, and a polling intake engine starts that workflow for each new item.
|
|
4
|
+
--
|
|
5
|
+
-- This is the emulator's OWN inbound state — deliberately NOT the classic
|
|
6
|
+
-- `automation*` tables (which live in cloud and feed the deprecated `/invoke`
|
|
7
|
+
-- pipeline). The model mirrors that pipeline's SHAPE (config + cursor state +
|
|
8
|
+
-- a delivery journal) without coupling to it: the classic engine stays running
|
|
9
|
+
-- and untouched; its retirement is tied to the World runtime's port to prod.
|
|
10
|
+
--
|
|
11
|
+
-- `trigger_subscriptions` holds the operator-owned link plus its polling state
|
|
12
|
+
-- (the opaque cursor + last-poll marker, owned by the platform; the
|
|
13
|
+
-- remote-subscription id is reserved for a future webhook/push source).
|
|
14
|
+
-- `trigger_deliveries` is the at-least-once dedup journal: one row per
|
|
15
|
+
-- (subscription, provider item) so two overlapping ticks or a mid-restart
|
|
16
|
+
-- re-poll never start the same item's workflow twice.
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.trigger_subscriptions (
|
|
19
|
+
id uuid PRIMARY KEY,
|
|
20
|
+
-- Integration id — the SAME id resolves the credential (broker) and the
|
|
21
|
+
-- trigger capability (connector kit). e.g. 'gmail'.
|
|
22
|
+
connector text NOT NULL,
|
|
23
|
+
-- Capability trigger id, e.g. 'message-received'. Not the reserved word
|
|
24
|
+
-- `trigger`, so the column is `trigger_id`.
|
|
25
|
+
trigger_id text NOT NULL,
|
|
26
|
+
-- Which broker grant to poll with: 'app' | 'user:<issuer>:<id>'.
|
|
27
|
+
principal_key text NOT NULL,
|
|
28
|
+
-- The World workflow this link starts.
|
|
29
|
+
workflow_name text NOT NULL,
|
|
30
|
+
-- Optional JSONata event→input mapping; identity when null.
|
|
31
|
+
mapping text,
|
|
32
|
+
enabled boolean NOT NULL DEFAULT true,
|
|
33
|
+
-- Opaque polling cursor (Gmail historyId, etc), platform-owned. Advanced
|
|
34
|
+
-- atomically at the END of a tick.
|
|
35
|
+
poll_cursor jsonb,
|
|
36
|
+
-- Reserved for a future push source (a provider-side subscription id); the
|
|
37
|
+
-- polling path never reads it.
|
|
38
|
+
remote_subscription_id text,
|
|
39
|
+
last_polled_at timestamptz,
|
|
40
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
41
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
-- The reconcile sweep loads ACTIVE links to arm one repeatable each.
|
|
45
|
+
CREATE INDEX IF NOT EXISTS trigger_subscriptions_enabled_idx
|
|
46
|
+
ON stackbone_platform.trigger_subscriptions (enabled, created_at)
|
|
47
|
+
WHERE enabled;
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.trigger_deliveries (
|
|
50
|
+
id uuid PRIMARY KEY,
|
|
51
|
+
subscription_id uuid NOT NULL,
|
|
52
|
+
-- The connector's stable provider item id (Gmail messageId) — the dedup key.
|
|
53
|
+
provider_item_id text NOT NULL,
|
|
54
|
+
status text NOT NULL DEFAULT 'claimed'
|
|
55
|
+
CHECK (status IN ('claimed', 'started', 'rejected')),
|
|
56
|
+
-- The projected World run id once the workflow started.
|
|
57
|
+
run_id text,
|
|
58
|
+
-- Validation/mapping issues when the delivery was rejected.
|
|
59
|
+
issues jsonb,
|
|
60
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
61
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
-- The at-least-once dedup guarantee: claiming a delivery is an INSERT that
|
|
65
|
+
-- collapses on this unique pair, so a re-polled item is a no-op, not a re-run.
|
|
66
|
+
CREATE UNIQUE INDEX IF NOT EXISTS trigger_deliveries_dedup_idx
|
|
67
|
+
ON stackbone_platform.trigger_deliveries (subscription_id, provider_item_id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackbone/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"pino": "^10.3.1",
|
|
35
35
|
"pino-pretty": "^13.1.3",
|
|
36
36
|
"postgres": "^3.4.9",
|
|
37
|
-
"
|
|
37
|
+
"semver": "^7.7.4",
|
|
38
|
+
"tar": "^7.5.13",
|
|
38
39
|
"yaml": "^2.8.3",
|
|
39
40
|
"zod": "^4.3.6"
|
|
40
41
|
}
|