@stackbone/cli 0.1.0-alpha.12 → 0.1.0-alpha.13

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.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbone/cli",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.13",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "pino": "^10.3.1",
35
35
  "pino-pretty": "^13.1.3",
36
36
  "postgres": "^3.4.9",
37
- "unpdf": "^0.12.1",
37
+ "tar": "^7.5.13",
38
38
  "yaml": "^2.8.3",
39
39
  "zod": "^4.3.6"
40
40
  }