stageflow 0.2.0 → 0.3.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/README.md +158 -14
- package/dist/agent/activity.d.ts +10 -1
- package/dist/agent/activity.js +37 -2
- package/dist/agent/activityObserver.d.ts +5 -1
- package/dist/agent/activityObserver.js +174 -11
- package/dist/agent/fakeAgent.js +4 -0
- package/dist/agent/piAdapter.d.ts +16 -0
- package/dist/agent/piAdapter.js +88 -21
- package/dist/agent/port.d.ts +2 -0
- package/dist/agent/providerAuth.js +12 -7
- package/dist/catalog/displayCatalogPath.d.ts +12 -0
- package/dist/catalog/displayCatalogPath.js +28 -0
- package/dist/cli/initCommand.d.ts +9 -0
- package/dist/cli/initCommand.js +71 -0
- package/dist/cli/initTemplates.d.ts +3 -0
- package/dist/cli/initTemplates.js +19 -0
- package/dist/cli/operatorCatalog.d.ts +10 -0
- package/dist/cli/operatorCatalog.js +16 -0
- package/dist/cli/runCommand.d.ts +3 -1
- package/dist/cli/runCommand.js +48 -7
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +20 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +52 -19
- package/dist/config/browseCatalog.d.ts +45 -0
- package/dist/config/browseCatalog.js +170 -0
- package/dist/config/createPipeline.d.ts +12 -3
- package/dist/config/createPipeline.js +292 -100
- package/dist/config/createStage.d.ts +10 -4
- package/dist/config/createStage.js +44 -14
- package/dist/config/listConfig.d.ts +14 -19
- package/dist/config/listConfig.js +26 -191
- package/dist/config/listModelsFromManifest.d.ts +2 -0
- package/dist/config/listModelsFromManifest.js +4 -0
- package/dist/config/loadOutcome.d.ts +4 -0
- package/dist/config/loadPipeline.d.ts +1 -15
- package/dist/config/loadPipeline.js +84 -142
- package/dist/config/loadStage.d.ts +4 -0
- package/dist/config/loadStage.js +60 -32
- package/dist/config/loadStageflowManifest.d.ts +5 -0
- package/dist/config/loadStageflowManifest.js +157 -0
- package/dist/config/mergePipelineIncludes.d.ts +9 -0
- package/dist/config/mergePipelineIncludes.js +141 -0
- package/dist/config/normalizePipelineStageEntry.d.ts +16 -0
- package/dist/config/normalizePipelineStageEntry.js +186 -0
- package/dist/config/pipelineStageKeys.d.ts +4 -0
- package/dist/config/pipelineStageKeys.js +14 -0
- package/dist/config/resolveCatalogContext.d.ts +13 -0
- package/dist/config/resolveCatalogContext.js +13 -0
- package/dist/config/resolveForkEmitContext.d.ts +3 -0
- package/dist/config/resolveForkEmitContext.js +12 -0
- package/dist/config/resolvePipelineDag.d.ts +5 -1
- package/dist/config/resolvePipelineDag.js +54 -36
- package/dist/config/scanCatalogPaths.d.ts +4 -0
- package/dist/config/scanCatalogPaths.js +83 -0
- package/dist/config/validateCatalog.d.ts +22 -4
- package/dist/config/validateCatalog.js +169 -132
- package/dist/envelope/check.js +7 -0
- package/dist/envelope/forkChoice.d.ts +3 -0
- package/dist/envelope/forkChoice.js +47 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/mcp/projectRun.d.ts +3 -0
- package/dist/mcp/projectRun.js +7 -0
- package/dist/mcp/tools.js +28 -11
- package/dist/project/findProjectRoot.d.ts +2 -0
- package/dist/project/findProjectRoot.js +51 -0
- package/dist/project/globalHome.d.ts +2 -0
- package/dist/project/globalHome.js +19 -0
- package/dist/project/resolveProjectContext.d.ts +9 -0
- package/dist/project/resolveProjectContext.js +38 -0
- package/dist/project/resolveStageflowContext.d.ts +11 -0
- package/dist/project/resolveStageflowContext.js +66 -0
- package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
- package/dist/runstore/normalizeCatalogPath.js +4 -0
- package/dist/runstore/port.d.ts +9 -0
- package/dist/runstore/runProjection.js +11 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +30 -4
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +4 -1
- package/dist/runtime/credentialBinding.d.ts +8 -6
- package/dist/runtime/credentialBinding.js +18 -24
- package/dist/runtime/pipelineRunner.d.ts +3 -0
- package/dist/runtime/pipelineRunner.js +15 -1
- package/dist/runtime/pipelineScheduler.d.ts +2 -0
- package/dist/runtime/pipelineScheduler.js +51 -11
- package/dist/runtime/reloadRunCatalog.d.ts +5 -0
- package/dist/runtime/reloadRunCatalog.js +44 -0
- package/dist/runtime/resumeReconstruct.d.ts +1 -0
- package/dist/runtime/resumeReconstruct.js +23 -7
- package/dist/runtime/runManager.d.ts +4 -0
- package/dist/runtime/runManager.js +36 -16
- package/dist/runtime/settingsFile.d.ts +12 -0
- package/dist/runtime/settingsFile.js +87 -6
- package/dist/runtime/stageAttemptBootstrap.js +3 -0
- package/dist/runtime/stageProcessLauncher.js +3 -0
- package/dist/runtime/stageRecovery.js +3 -1
- package/dist/runtime/stageWorker.js +2 -6
- package/dist/server/http.js +33 -9
- package/dist/tools/emitStageEnvelope.d.ts +3 -1
- package/dist/tools/emitStageEnvelope.js +11 -1
- package/dist/types/envelope.d.ts +1 -0
- package/dist/types/forkChoice.d.ts +8 -0
- package/dist/types/forkChoice.js +1 -0
- package/dist/types/pipeline.d.ts +51 -2
- package/dist/types/stageflowManifest.d.ts +23 -0
- package/dist/types/stageflowManifest.js +1 -0
- package/dist/ui/assets/index-CFSzDZje.js +118 -0
- package/dist/ui/assets/{index-DCsDopak.css → index-DefBlEvN.css} +1 -1
- package/dist/ui/index.html +3 -2
- package/dist/ui/stageflow-icon.svg +12 -0
- package/package.json +7 -5
- package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
package/README.md
CHANGED
|
@@ -1,12 +1,54 @@
|
|
|
1
|
-
# Stageflow
|
|
1
|
+
# <img src="ui/public/stageflow-icon.svg" alt="" width="44" height="44" valign="middle"> Stageflow
|
|
2
2
|
|
|
3
|
-
CLI pipeline runtime for configurable
|
|
3
|
+
CLI pipeline runtime for **configurable stages** on [Pi](https://github.com/badlogic/pi-mono), with a local operator console.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/stageflow)
|
|
6
|
+
[](https://www.npmjs.com/package/stageflow)
|
|
7
|
+

|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://github.com/tejasghutukade/stageflow/issues)
|
|
10
|
+
[](https://github.com/tejasghutukade/stageflow/actions/workflows/ci.yml)
|
|
11
|
+
|
|
12
|
+
Author pipeline-owned YAML in your project. Stageflow runs each stage in a fresh Pi agent session, writes a structured handoff envelope, and keeps run state under `.stageflow/`. Bins: **`sf`** and **`stageflow`**.
|
|
13
|
+
|
|
14
|
+
## Why Stageflow?
|
|
15
|
+
|
|
16
|
+
Multi-step agent work breaks down when every handoff is ad hoc — a shell script here, a chat transcript there, no shared contract between steps. You end up re-explaining context, losing artifacts, and unable to run the same flow locally and in CI.
|
|
17
|
+
|
|
18
|
+
Stageflow treats **stages as the unit of composition**. You author pipeline-owned YAML — `*.pipeline.yaml`, `*.task.yaml`, optional `stageflow.yaml` manifest — and define whatever workflow fits your domain: release automation, research pipelines, content review, SDLC, ops runbooks, or something entirely custom. Each stage runs in a **fresh Pi session**, emits a typed **envelope** for the next stage, and can pause on **human-in-the-loop (HITL)** gates when you need an operator in the loop.
|
|
19
|
+
|
|
20
|
+
The same pipeline runs three ways without rewriting anything:
|
|
21
|
+
|
|
22
|
+
- **Locally** — `sf ui` for triage, provider setup, and gate replies
|
|
23
|
+
- **Headless / CI** — `sf validate` and `sf run --json` with predictable exit codes
|
|
24
|
+
- **Via MCP** — Streamable HTTP tools when the console is running
|
|
25
|
+
|
|
26
|
+
**Stageflow is not an SDLC tool.** Software delivery is a popular pattern in fixtures and dogfood flows, but stages are user-authored and domain-agnostic. If you can express a multi-step workflow in YAML, Stageflow can run it on Pi.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- **Pipeline-owned YAML** — `*.pipeline.yaml` with inline stages or `uses:` refs; separate `*.task.yaml` files; optional repo-root `stageflow.yaml` manifest
|
|
31
|
+
- **Path-based CLI** — `--pipeline` and `--task` take filesystem paths (no bare-id lookup)
|
|
32
|
+
- **Pi-native** — runs on `@earendil-works/pi-coding-agent`; reuse an existing Pi login (`pi_home`) or store credentials in `~/.stageflow/agent/auth.json` (`sf_owned`)
|
|
33
|
+
- **Envelope handoffs** — typed stage payloads and artifacts via `write_stage_artifact` / `emit_stage_envelope`
|
|
34
|
+
- **HITL gates** — operator questions in the console; CI exits `2` when a run is waiting
|
|
35
|
+
- **Operator console** — triage runs, connect providers, answer gates, inspect transcripts at `http://127.0.0.1:3847`
|
|
36
|
+
- **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` is running
|
|
37
|
+
- **CI / headless** — `sf validate --strict --json`, `sf run --json` with exit codes `0` / `1` / `2`
|
|
38
|
+
- **Parallel stages** — pipeline DAG with fan-out and join (see [YAML catalog](docs/yaml-catalog.md))
|
|
39
|
+
- **SQLite run store** — `<git-root>/.stageflow/` state plus per-run workspaces under `.stageflow/runs/`
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
6
42
|
|
|
7
43
|
Requires **Node.js ≥ 20**.
|
|
8
44
|
|
|
9
|
-
|
|
45
|
+
**Quick install (macOS / Linux):**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
curl -fsSL https://raw.githubusercontent.com/tejasghutukade/stageflow/main/install.sh | bash
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**npm:**
|
|
10
52
|
|
|
11
53
|
```bash
|
|
12
54
|
npm i -g stageflow
|
|
@@ -20,18 +62,67 @@ npm i -g ./stageflow-*.tgz
|
|
|
20
62
|
|
|
21
63
|
## Quick start
|
|
22
64
|
|
|
23
|
-
In a project directory
|
|
65
|
+
In a project directory (preferably a git repo):
|
|
24
66
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
67
|
+
```bash
|
|
68
|
+
sf init
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This scaffolds `stageflow.yaml`, `pipelines/hello.pipeline.yaml` (inline stage), and `tasks/hello.task.yaml`.
|
|
72
|
+
|
|
73
|
+
Run (after connecting a provider — see below):
|
|
28
74
|
|
|
29
75
|
```bash
|
|
30
76
|
sf ui # operator console at http://127.0.0.1:3847
|
|
31
|
-
sf run --
|
|
77
|
+
sf run --pipeline pipelines/hello.pipeline.yaml --task tasks/hello.task.yaml
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Expanded walkthrough: [docs/quickstart.md](docs/quickstart.md)
|
|
81
|
+
|
|
82
|
+
## Connect a model provider
|
|
83
|
+
|
|
84
|
+
Each stage sets a **`model`** id in YAML (e.g. `anthropic/claude-sonnet-4-5`). The matching provider must be authenticated before runs succeed — `sf validate` does not check auth.
|
|
85
|
+
|
|
86
|
+
**Operator console** (easiest for local setup):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
sf ui
|
|
32
90
|
```
|
|
33
91
|
|
|
34
|
-
|
|
92
|
+
Open **Settings → Providers** (or **Connect** from the rail when nothing is configured) and sign in with API key or OAuth.
|
|
93
|
+
|
|
94
|
+
**CLI** (works headless and in CI):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
sf providers list
|
|
98
|
+
sf providers login anthropic --type api_key
|
|
99
|
+
sf providers login anthropic --type api_key --api-key-env ANTHROPIC_API_KEY
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use `sf providers list` to see provider ids and supported auth types (`api_key`, `oauth`).
|
|
103
|
+
|
|
104
|
+
**Credential storage:** reuse Pi's shared auth file (`pi_home`) or keep credentials in Stageflow's global store (`sf_owned`):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
sf providers detect
|
|
108
|
+
sf providers source set pi_home # or sf_owned
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Stageflow is a thin Pi shell — you do not need Pi CLI `/login` as a hard prerequisite if you configure providers via the console or `sf providers`.
|
|
112
|
+
|
|
113
|
+
Full reference: [docs/providers.md](docs/providers.md)
|
|
114
|
+
|
|
115
|
+
## Operator console
|
|
116
|
+
|
|
117
|
+
Start the console with `sf ui` (default `http://127.0.0.1:3847`).
|
|
118
|
+
|
|
119
|
+
- **Runs** — active and recent pipeline runs, capacity, and status at a glance
|
|
120
|
+
- **Run detail** — stage timeline, transcripts, envelope payloads, and artifact paths
|
|
121
|
+
- **HITL reply** — answer operator gates (`ask_operator`) without leaving the browser
|
|
122
|
+
- **Pipelines** — browse manifest-declared pipeline definitions
|
|
123
|
+
- **Settings → Providers** — connect model providers (`pi_home` or `sf_owned` credential storage)
|
|
124
|
+
|
|
125
|
+
*Screenshot coming soon — capture after console polish lands (see `docs/img/`).*
|
|
35
126
|
|
|
36
127
|
## Headless / CI
|
|
37
128
|
|
|
@@ -50,7 +141,7 @@ sf providers login <providerId> --api-key-env <VAR>
|
|
|
50
141
|
If the provider also supports OAuth, pass `--type api_key`.
|
|
51
142
|
|
|
52
143
|
```bash
|
|
53
|
-
sf run --
|
|
144
|
+
sf run --pipeline pipelines/hello.pipeline.yaml --task tasks/hello.task.yaml --json
|
|
54
145
|
```
|
|
55
146
|
|
|
56
147
|
The process exits `0` when the Run succeeded, `1` when it failed (including a busy start), and `2` when waiting. `sf run --json` prints one stdout document. `ok` is true only for `succeeded`. Busy has no `runId`.
|
|
@@ -66,11 +157,60 @@ On a mixed Pipeline, default wait parks the Run (exit `2`). `--skip-gates` fails
|
|
|
66
157
|
|
|
67
158
|
## State
|
|
68
159
|
|
|
69
|
-
Runtime state lives in
|
|
160
|
+
Runtime state lives in **`<git-root>/.stageflow/`** when inside a git repository (SQLite + per-run workspaces under `.stageflow/runs/`). Global config and `sf_owned` auth live under **`~/.stageflow/`**. If `.stageflow` is missing and `.software-factory` exists from an older install, the next store open renames it to `.stageflow` once.
|
|
70
161
|
|
|
71
162
|
## MCP
|
|
72
163
|
|
|
73
|
-
`sf ui` also serves a Streamable HTTP MCP endpoint at `http://127.0.0.1:3847/mcp` (URL printed on boot). Point a Cursor (or other) MCP client at that URL
|
|
164
|
+
`sf ui` also serves a Streamable HTTP MCP endpoint at `http://127.0.0.1:3847/mcp` (URL printed on boot). Point a Cursor (or other) MCP client at that URL.
|
|
165
|
+
|
|
166
|
+
Available tools: `list_pipelines`, `list_tasks`, `list_runs`, `get_health`, `start_run`, `get_run`, `read_artifact`.
|
|
167
|
+
|
|
168
|
+
Full reference: [docs/mcp.md](docs/mcp.md)
|
|
169
|
+
|
|
170
|
+
## Stageflow vs Conductor
|
|
171
|
+
|
|
172
|
+
Both projects address multi-step agent workflows. They differ in orchestration model and runtime.
|
|
173
|
+
|
|
174
|
+
| | **Stageflow** | **Conductor** |
|
|
175
|
+
|---|---------------|---------------|
|
|
176
|
+
| **Model** | Configurable **stages** on Pi; pipeline-owned YAML (any domain) | Multi-**agent** workflow graph |
|
|
177
|
+
| **Orchestration** | Pipeline DAG + stage worker | Jinja routing, no LLM in router |
|
|
178
|
+
| **Unit of work** | Task → Pipeline → Stage attempts | Workflow → Agents |
|
|
179
|
+
| **Handoff** | Typed **envelope** + artifacts | Agent output → context |
|
|
180
|
+
| **Human gates** | Operator console + MCP | Dashboard + TUI fleet |
|
|
181
|
+
| **Runtime** | Node.js, Pi coding agent | Python, Copilot/Claude SDKs |
|
|
182
|
+
| **Best for** | Personal/team **multi-stage Pi workflows** you define (releases, research, SDLC, …) | Enterprise multi-agent workflows |
|
|
183
|
+
|
|
184
|
+
If you want deterministic YAML routing across many agents, look at [Conductor](https://github.com/microsoft/conductor). If you want stage-bound Pi runs with reviewable envelopes and an operator console for **workflows you author**, use Stageflow.
|
|
185
|
+
|
|
186
|
+
## Examples
|
|
187
|
+
|
|
188
|
+
| Example | Description |
|
|
189
|
+
|---------|-------------|
|
|
190
|
+
| [hello-world](examples/hello-world/) | Single stage, domain-neutral |
|
|
191
|
+
| [plan-review](examples/plan-review/) | Multi-stage with operator gate — SDLC-style **example** |
|
|
192
|
+
| [github-release](examples/github-release/) | Dogfood: draft + publish GitHub Release |
|
|
193
|
+
| [ci-validate](examples/ci-validate/) | Strict validate in CI |
|
|
194
|
+
|
|
195
|
+
Index: [examples/README.md](examples/README.md)
|
|
196
|
+
|
|
197
|
+
## Documentation
|
|
198
|
+
|
|
199
|
+
Full docs: **[tejasghutukade.github.io/stageflow](https://tejasghutukade.github.io/stageflow/)** (GitHub Pages — live after merge to `main` and Pages enabled). Source in [`docs/`](docs/).
|
|
200
|
+
|
|
201
|
+
| Doc | Description |
|
|
202
|
+
|-----|-------------|
|
|
203
|
+
| [docs/README.md](docs/README.md) | Documentation index |
|
|
204
|
+
| [docs/quickstart.md](docs/quickstart.md) | Expanded quick start |
|
|
205
|
+
| [docs/yaml-catalog.md](docs/yaml-catalog.md) | Pipelines, stages, tasks schema |
|
|
206
|
+
| [docs/cli-reference.md](docs/cli-reference.md) | `sf run`, `sf validate`, `sf ui`, `sf providers` |
|
|
207
|
+
| [docs/envelopes.md](docs/envelopes.md) | Handoff envelope contract |
|
|
208
|
+
| [docs/hitl.md](docs/hitl.md) | Gate kinds, `--skip-gates`, exit code `2` |
|
|
209
|
+
| [docs/ci.md](docs/ci.md) | `--json`, env vars, GitHub Actions |
|
|
210
|
+
| [docs/mcp.md](docs/mcp.md) | MCP tool reference |
|
|
211
|
+
| [docs/providers.md](docs/providers.md) | Pi providers, `sf providers` |
|
|
212
|
+
| [docs/operator-console.md](docs/operator-console.md) | Console IA and settings |
|
|
213
|
+
| [docs/compare-conductor.md](docs/compare-conductor.md) | Positioning deep dive |
|
|
74
214
|
|
|
75
215
|
## Develop from source
|
|
76
216
|
|
|
@@ -82,8 +222,12 @@ npm run build && npm run ui:build # ui:build builds the UI and copies assets i
|
|
|
82
222
|
sf ui
|
|
83
223
|
```
|
|
84
224
|
|
|
225
|
+
Run tests: `npm test` and `npm run ui:test`. Typecheck: `npm run typecheck`.
|
|
226
|
+
|
|
85
227
|
## License
|
|
86
228
|
|
|
87
229
|
MIT © Tejas G
|
|
88
230
|
|
|
89
|
-
Issues
|
|
231
|
+
**Support:** [GitHub Issues](https://github.com/tejasghutukade/stageflow/issues) · [SUPPORT.md](SUPPORT.md)
|
|
232
|
+
|
|
233
|
+
**Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
|
package/dist/agent/activity.d.ts
CHANGED
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* those Q&A bodies.
|
|
12
12
|
*/
|
|
13
13
|
import type { AskOperatorAnswer, AskOperatorPrompt } from "../tools/askOperator.js";
|
|
14
|
-
export declare const ACTIVITY_TEXT_LIMIT =
|
|
14
|
+
export declare const ACTIVITY_TEXT_LIMIT = 2000;
|
|
15
|
+
export declare const ACTIVITY_TEXT_LIMIT_ENV = "STAGEFLOW_ACTIVITY_TEXT_LIMIT";
|
|
16
|
+
export declare const ACTIVITY_VERBOSE_ENV = "STAGEFLOW_ACTIVITY_VERBOSE";
|
|
17
|
+
export declare function readActivityTextLimit(env?: Record<string, string | undefined>, override?: number): number;
|
|
18
|
+
export declare function readActivityVerbose(env?: Record<string, string | undefined>, override?: boolean): boolean;
|
|
15
19
|
export type StageActivityEvent = {
|
|
16
20
|
event: "agent_start";
|
|
17
21
|
} | {
|
|
@@ -29,6 +33,11 @@ export type StageActivityEvent = {
|
|
|
29
33
|
toolCallId?: string;
|
|
30
34
|
isError?: boolean;
|
|
31
35
|
resultPreview?: string;
|
|
36
|
+
} | {
|
|
37
|
+
event: "tool_progress";
|
|
38
|
+
toolName: string;
|
|
39
|
+
toolCallId?: string;
|
|
40
|
+
textPreview?: string;
|
|
32
41
|
} | {
|
|
33
42
|
event: "message";
|
|
34
43
|
role: string;
|
package/dist/agent/activity.js
CHANGED
|
@@ -10,8 +10,43 @@
|
|
|
10
10
|
* ACTIVITY_TEXT_LIMIT applies only to tool/message *previews* — never to
|
|
11
11
|
* those Q&A bodies.
|
|
12
12
|
*/
|
|
13
|
-
export const ACTIVITY_TEXT_LIMIT =
|
|
14
|
-
export
|
|
13
|
+
export const ACTIVITY_TEXT_LIMIT = 2000;
|
|
14
|
+
export const ACTIVITY_TEXT_LIMIT_ENV = "STAGEFLOW_ACTIVITY_TEXT_LIMIT";
|
|
15
|
+
export const ACTIVITY_VERBOSE_ENV = "STAGEFLOW_ACTIVITY_VERBOSE";
|
|
16
|
+
export function readActivityTextLimit(env = process.env, override) {
|
|
17
|
+
if (override !== undefined) {
|
|
18
|
+
if (Number.isFinite(override) && override >= 1)
|
|
19
|
+
return Math.floor(override);
|
|
20
|
+
return ACTIVITY_TEXT_LIMIT;
|
|
21
|
+
}
|
|
22
|
+
const raw = env[ACTIVITY_TEXT_LIMIT_ENV];
|
|
23
|
+
if (raw === undefined || raw.trim() === "") {
|
|
24
|
+
return ACTIVITY_TEXT_LIMIT;
|
|
25
|
+
}
|
|
26
|
+
const n = Number.parseInt(raw, 10);
|
|
27
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
28
|
+
return ACTIVITY_TEXT_LIMIT;
|
|
29
|
+
}
|
|
30
|
+
return n;
|
|
31
|
+
}
|
|
32
|
+
export function readActivityVerbose(env = process.env, override) {
|
|
33
|
+
if (override !== undefined) {
|
|
34
|
+
return override;
|
|
35
|
+
}
|
|
36
|
+
const raw = env[ACTIVITY_VERBOSE_ENV];
|
|
37
|
+
if (raw === undefined || raw.trim() === "") {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const normalized = raw.trim().toLowerCase();
|
|
41
|
+
if (normalized === "0" ||
|
|
42
|
+
normalized === "false" ||
|
|
43
|
+
normalized === "off" ||
|
|
44
|
+
normalized === "no") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
export function truncateActivityText(value, limit = readActivityTextLimit()) {
|
|
15
50
|
if (value === undefined || value === null)
|
|
16
51
|
return undefined;
|
|
17
52
|
let text;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type StageActivityEvent } from "./activity.js";
|
|
2
|
+
export declare const TOOL_PROGRESS_THROTTLE_MS = 300;
|
|
2
3
|
export type StageActivityObserver = {
|
|
3
4
|
onAssistantTextDelta(delta: string): void;
|
|
5
|
+
onThinkingDelta(delta: string): void;
|
|
6
|
+
onToolPartialResult(accumulatedText: string): void;
|
|
4
7
|
onStreamBoundary(): void;
|
|
5
8
|
onActivity(activity: StageActivityEvent): void;
|
|
6
9
|
dispose(): void;
|
|
@@ -8,6 +11,7 @@ export type StageActivityObserver = {
|
|
|
8
11
|
export type CreateStageActivityObserverOptions = {
|
|
9
12
|
onActivity?: (event: StageActivityEvent) => void;
|
|
10
13
|
writeStderr?: boolean;
|
|
14
|
+
toolProgressThrottleMs?: number;
|
|
11
15
|
};
|
|
12
16
|
/**
|
|
13
17
|
* Observe StageActivityEvent milestones and optional assistant text deltas.
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { truncateActivityText, } from "./activity.js";
|
|
2
|
+
export const TOOL_PROGRESS_THROTTLE_MS = 300;
|
|
3
|
+
function writeIndentedPreview(preview) {
|
|
4
|
+
for (const line of preview.split("\n")) {
|
|
5
|
+
process.stderr.write(` ${line}\n`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function writeActivityStderr(activity, skipAssistantMessage) {
|
|
2
9
|
switch (activity.event) {
|
|
3
10
|
case "agent_start":
|
|
4
11
|
process.stderr.write(" … agent started\n");
|
|
@@ -11,13 +18,33 @@ function writeActivityStderr(activity) {
|
|
|
11
18
|
return;
|
|
12
19
|
case "tool_start":
|
|
13
20
|
process.stderr.write(` → ${activity.toolName}\n`);
|
|
21
|
+
if (activity.argsPreview) {
|
|
22
|
+
writeIndentedPreview(activity.argsPreview);
|
|
23
|
+
}
|
|
14
24
|
return;
|
|
15
25
|
case "tool_end":
|
|
16
26
|
if (activity.isError) {
|
|
17
27
|
process.stderr.write(` ✕ ${activity.toolName}\n`);
|
|
18
28
|
}
|
|
29
|
+
else if (activity.resultPreview) {
|
|
30
|
+
process.stderr.write(` ← ${activity.toolName}\n`);
|
|
31
|
+
}
|
|
32
|
+
if (activity.resultPreview) {
|
|
33
|
+
writeIndentedPreview(activity.resultPreview);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
case "tool_progress":
|
|
19
37
|
return;
|
|
20
38
|
case "message":
|
|
39
|
+
if (activity.role === "assistant" && skipAssistantMessage) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (activity.role === "thinking") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (activity.text) {
|
|
46
|
+
writeIndentedPreview(activity.text);
|
|
47
|
+
}
|
|
21
48
|
return;
|
|
22
49
|
}
|
|
23
50
|
}
|
|
@@ -28,39 +55,175 @@ function writeActivityStderr(activity) {
|
|
|
28
55
|
*/
|
|
29
56
|
export function createStageActivityObserver(options = {}) {
|
|
30
57
|
const writeStderr = options.writeStderr !== false;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
58
|
+
const toolProgressThrottleMs = options.toolProgressThrottleMs ?? TOOL_PROGRESS_THROTTLE_MS;
|
|
59
|
+
let openStream = "none";
|
|
60
|
+
let assistantStreamedForMessage = false;
|
|
61
|
+
let lastToolPartial = "";
|
|
62
|
+
let thinkingBuffer = "";
|
|
63
|
+
let openTool = null;
|
|
64
|
+
let toolProgressAccum = "";
|
|
65
|
+
let toolProgressPending = false;
|
|
66
|
+
let toolProgressTimer = null;
|
|
67
|
+
const endOpenStream = () => {
|
|
68
|
+
if (openStream === "none") {
|
|
34
69
|
return;
|
|
35
70
|
}
|
|
36
71
|
if (writeStderr) {
|
|
37
72
|
process.stderr.write("\n");
|
|
38
73
|
}
|
|
39
|
-
|
|
74
|
+
openStream = "none";
|
|
75
|
+
};
|
|
76
|
+
const flushThinking = () => {
|
|
77
|
+
const raw = thinkingBuffer;
|
|
78
|
+
thinkingBuffer = "";
|
|
79
|
+
if (!raw.trim()) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
options.onActivity?.({
|
|
83
|
+
event: "message",
|
|
84
|
+
role: "thinking",
|
|
85
|
+
text: truncateActivityText(raw),
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const emitToolProgress = () => {
|
|
89
|
+
if (!toolProgressPending || !openTool) {
|
|
90
|
+
toolProgressPending = false;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const textPreview = truncateActivityText(toolProgressAccum);
|
|
94
|
+
toolProgressPending = false;
|
|
95
|
+
if (!textPreview?.trim()) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
options.onActivity?.({
|
|
99
|
+
event: "tool_progress",
|
|
100
|
+
toolName: openTool.toolName,
|
|
101
|
+
toolCallId: openTool.toolCallId,
|
|
102
|
+
textPreview,
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const cancelToolProgressTimer = () => {
|
|
106
|
+
if (toolProgressTimer !== null) {
|
|
107
|
+
clearTimeout(toolProgressTimer);
|
|
108
|
+
toolProgressTimer = null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const flushToolProgress = () => {
|
|
112
|
+
cancelToolProgressTimer();
|
|
113
|
+
emitToolProgress();
|
|
114
|
+
};
|
|
115
|
+
const scheduleToolProgress = () => {
|
|
116
|
+
if (!openTool || !toolProgressAccum.trim()) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
toolProgressPending = true;
|
|
120
|
+
if (toolProgressTimer !== null) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
toolProgressTimer = setTimeout(() => {
|
|
124
|
+
toolProgressTimer = null;
|
|
125
|
+
emitToolProgress();
|
|
126
|
+
}, toolProgressThrottleMs);
|
|
40
127
|
};
|
|
41
128
|
return {
|
|
42
129
|
onAssistantTextDelta(delta) {
|
|
43
130
|
if (!writeStderr || !delta) {
|
|
44
131
|
return;
|
|
45
132
|
}
|
|
46
|
-
|
|
133
|
+
assistantStreamedForMessage = true;
|
|
134
|
+
if (openStream !== "assistant") {
|
|
135
|
+
endOpenStream();
|
|
47
136
|
process.stderr.write(" ");
|
|
48
|
-
|
|
137
|
+
openStream = "assistant";
|
|
138
|
+
}
|
|
139
|
+
process.stderr.write(delta);
|
|
140
|
+
},
|
|
141
|
+
onThinkingDelta(delta) {
|
|
142
|
+
if (!delta) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
thinkingBuffer += delta;
|
|
146
|
+
if (!writeStderr) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (openStream !== "thinking") {
|
|
150
|
+
endOpenStream();
|
|
151
|
+
process.stderr.write(" ∴ ");
|
|
152
|
+
openStream = "thinking";
|
|
153
|
+
}
|
|
154
|
+
process.stderr.write(delta);
|
|
155
|
+
},
|
|
156
|
+
onToolPartialResult(accumulatedText) {
|
|
157
|
+
if (!accumulatedText) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
toolProgressAccum = accumulatedText;
|
|
161
|
+
scheduleToolProgress();
|
|
162
|
+
if (!writeStderr) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const delta = accumulatedText.startsWith(lastToolPartial)
|
|
166
|
+
? accumulatedText.slice(lastToolPartial.length)
|
|
167
|
+
: accumulatedText;
|
|
168
|
+
lastToolPartial = accumulatedText;
|
|
169
|
+
if (!delta) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (openStream !== "tool") {
|
|
173
|
+
endOpenStream();
|
|
174
|
+
process.stderr.write(" │ ");
|
|
175
|
+
openStream = "tool";
|
|
49
176
|
}
|
|
50
177
|
process.stderr.write(delta);
|
|
51
178
|
},
|
|
52
179
|
onStreamBoundary() {
|
|
53
|
-
|
|
180
|
+
endOpenStream();
|
|
181
|
+
flushThinking();
|
|
182
|
+
flushToolProgress();
|
|
183
|
+
lastToolPartial = "";
|
|
54
184
|
},
|
|
55
185
|
onActivity(activity) {
|
|
56
|
-
|
|
186
|
+
flushThinking();
|
|
187
|
+
if (activity.event === "tool_end") {
|
|
188
|
+
flushToolProgress();
|
|
189
|
+
}
|
|
190
|
+
endOpenStream();
|
|
191
|
+
lastToolPartial = "";
|
|
192
|
+
if (activity.event === "turn_start") {
|
|
193
|
+
assistantStreamedForMessage = false;
|
|
194
|
+
}
|
|
195
|
+
if (activity.event === "tool_start") {
|
|
196
|
+
openTool = {
|
|
197
|
+
toolName: activity.toolName,
|
|
198
|
+
toolCallId: activity.toolCallId,
|
|
199
|
+
};
|
|
200
|
+
toolProgressAccum = "";
|
|
201
|
+
toolProgressPending = false;
|
|
202
|
+
cancelToolProgressTimer();
|
|
203
|
+
}
|
|
204
|
+
else if (activity.event === "tool_end") {
|
|
205
|
+
openTool = null;
|
|
206
|
+
toolProgressAccum = "";
|
|
207
|
+
toolProgressPending = false;
|
|
208
|
+
cancelToolProgressTimer();
|
|
209
|
+
}
|
|
210
|
+
const skipAssistantMessage = activity.event === "message" &&
|
|
211
|
+
activity.role === "assistant" &&
|
|
212
|
+
assistantStreamedForMessage;
|
|
213
|
+
if (activity.event === "message" &&
|
|
214
|
+
activity.role === "assistant") {
|
|
215
|
+
assistantStreamedForMessage = false;
|
|
216
|
+
}
|
|
57
217
|
options.onActivity?.(activity);
|
|
58
218
|
if (writeStderr) {
|
|
59
|
-
writeActivityStderr(activity);
|
|
219
|
+
writeActivityStderr(activity, skipAssistantMessage);
|
|
60
220
|
}
|
|
61
221
|
},
|
|
62
222
|
dispose() {
|
|
63
|
-
|
|
223
|
+
endOpenStream();
|
|
224
|
+
flushThinking();
|
|
225
|
+
flushToolProgress();
|
|
226
|
+
cancelToolProgressTimer();
|
|
64
227
|
},
|
|
65
228
|
};
|
|
66
229
|
}
|
package/dist/agent/fakeAgent.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertRequiredEnvelope, isAdvancingEnvelope, } from "../envelope/check.js";
|
|
4
4
|
import { assertEnvelopePayload } from "../envelope/payloadSchema.js";
|
|
5
|
+
import { assertForkEnvelope } from "../envelope/forkChoice.js";
|
|
5
6
|
import { assertAnswerMatchesPrompt, parseAskOperatorAnswer, } from "../tools/askOperator.js";
|
|
6
7
|
import { tryParsePendingPrompt } from "../runtime/stageHitl.js";
|
|
7
8
|
import { runStageViaOpen } from "./port.js";
|
|
@@ -135,6 +136,9 @@ export class FakeAgent {
|
|
|
135
136
|
: undefined;
|
|
136
137
|
try {
|
|
137
138
|
const envelope = assertRequiredEnvelope(envelopeValue);
|
|
139
|
+
if (input.forkEmitContext !== undefined) {
|
|
140
|
+
assertForkEnvelope(envelope, input.forkEmitContext);
|
|
141
|
+
}
|
|
138
142
|
assertEnvelopePayload(envelope, input.stage.payload_schema);
|
|
139
143
|
if (!isAdvancingEnvelope(envelope)) {
|
|
140
144
|
input.onActivity?.({ event: "agent_end" });
|
|
@@ -2,6 +2,7 @@ import { type AgentSession, DefaultResourceLoader, SessionManager, SettingsManag
|
|
|
2
2
|
import type { StageRoots } from "../runtime/stageRoots.js";
|
|
3
3
|
import { type AskOperatorPrompt, type AskOperatorWaitBridge } from "../tools/askOperator.js";
|
|
4
4
|
import "./cursorProvider.js";
|
|
5
|
+
import { type StageActivityObserver } from "./activityObserver.js";
|
|
5
6
|
import type { AgentPort, OpaqueAnswer, StageHandle, StageHandleCloseOptions, StageRunInput, StageRunResult } from "./port.js";
|
|
6
7
|
/**
|
|
7
8
|
* Stage tool allowlist for sealed Pi sessions.
|
|
@@ -45,6 +46,21 @@ export type ConnectedAskWaitStageHandleOptions = {
|
|
|
45
46
|
* runtime never imports the channel.
|
|
46
47
|
*/
|
|
47
48
|
export declare function createConnectedAskWaitStageHandle(options: ConnectedAskWaitStageHandleOptions): StageHandle;
|
|
49
|
+
/**
|
|
50
|
+
* Extract display text from a Pi tool_execution_update partialResult.
|
|
51
|
+
* Prefers content[].text blocks; otherwise JSON-stringifies.
|
|
52
|
+
*/
|
|
53
|
+
export declare function extractPartialResultText(partialResult: unknown): string;
|
|
54
|
+
export type RouteSessionEventToProgressOptions = {
|
|
55
|
+
observer: StageActivityObserver;
|
|
56
|
+
verbose: boolean;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Adapter-edge routing: stream deltas to the observer, map milestones to
|
|
60
|
+
* StageActivityEvent. Verbose thinking / live tool partials go to observer
|
|
61
|
+
* hooks (stderr streams + coalesced/throttled persist via onActivity).
|
|
62
|
+
*/
|
|
63
|
+
export declare function routeSessionEventToProgress(event: Record<string, unknown>, options: RouteSessionEventToProgressOptions): void;
|
|
48
64
|
export declare function composeStageUserPrompt(input: StageRunInput, emitToolName: string, emitHintOverride?: string, artifactToolName?: string): string;
|
|
49
65
|
/**
|
|
50
66
|
* DefaultResourceLoader with host/global discovery turned off.
|