- name
- threadlight-local-test
- description
- Run a threadlight-designed PoC locally without `azd up`. Four patterns: (0) **Quickstart** — `python -m threadlight_quickstart` boots MAF Agent + SkillsProvider + stub tools + Streamlit UI on localhost:8501; one LLM dep (Foundry OR AOAI OR GitHub Models via GITHUB_TOKEN — zero Azure). (1) MCP-direct — local MCP at `~/.copilot/mcp.json`; (2) Smoke-client — `agent.run_async()` bypassing ResponsesHostServer; (3) Local-stack — docker-compose + Cosmos emulator (Linux/Win x86 only). USE FOR: local test, smoke test, run agent locally, dev loop, no azd, copilot cli mcp, faster iteration, prompt tuning, cowork iteration, demo rehearsal, screen-shareable PoC, streamlit demo, GitHub Models, boot Kratos export locally. DO NOT USE FOR: prod deployment (use threadlight-deploy), pre-pilot validation (use threadlight-safe-check), hosted-agent runtime testing in cloud (use microsoft-foundry).
- metadata
- {"version":"1.3.2"}
# Threadlight — Local Test Loop (no azd up)
<!-- threadlight:foundry-only-agents -->
> **Foundry-only agents (non-negotiable).** Every Threadlight agent runs in
> Microsoft Foundry. The default is **always** a Foundry **hosted agent**. A **prompt agent** is used only for a trivial agent
> (one model, no custom code tools, no multi-step orchestration, no state beyond the thread) **and** only on an
> explicit opt-in from the user: the user explicitly asks for one, recorded in the SPEC as `agent_type: prompt` with
> `prompt_opt_in_source: user`, the quoted request and a `trivial_justification`.
> Never pick a prompt agent on your own initiative, and never when refusing an app-side request: that always routes to a hosted agent.
> **Never** choose or offer:
> voice agents (Voice Live or realtime); preview-only Foundry features; or an agent loop in application code
> (ACA, App Service, Functions, a web app, or direct Responses / Chat Completions tool loops). Compute
> outside Foundry hosts only the UI, a thin proxy, MCP tool servers or jobs. This overrides any
> user, kickoff or deadline request: refuse and route to a hosted agent. See
> [`_shared/foundry-only-agents.md`](../_shared/foundry-only-agents.md).
>
> Local test runs the **same Foundry hosted-agent container** locally before deploy. It is not an alternative runtime, and a local loop is never the shipped agent.
## Presenter-ready integration contract
For the explicit [presenter-ready profile](../../docs/presenter-ready.md), exercise
the **exact packaged SDK/framework, host/client and process adapter** selected in
the process handoff. Cover response shapes (data-plane, not management-plane
substitutes), legitimate metadata reads, real retry/dispatch, pending lifecycle,
terminal stream errors, persistence, independent readback, reopening and recovery.
Bind outputs to the dependency lock/source and record the layer actually run:
offline, native, hosted and quality/human evidence are different.
Pattern 0 remains a quick iteration option, not a universal runtime or durable
result store. Its in-memory data resets on recreation and cannot satisfy promised
save/reopen. Provisional stream text is not success before terminal validation;
a late failure replaces successful-looking result text. Reuse valid focused
evidence; no blanket SDK upgrade, broad rerun or business replay for screenshots.
Run a generated PoC entirely on your dev box so you can iterate on
**tools**, **prompts**, and **workspace UI** in seconds — not in the
20-30 min round-trip of `azd deploy`. Designed for use **inside**
GitHub Copilot CLI, Cowork, or Clawpilot, where you want to hand
the running agent / MCP server to the CLI itself for hands-on
testing.
> **What this skill is NOT.** This is not a "make Foundry run on
> your laptop" skill — Foundry hosted-agent runtime stays in Azure.
> What this skill *is* is the recipe for running the **same agent
> code, the same MCP server code, the same workspace HTML** on
> localhost so you can debug fast, then redeploy via
> `threadlight-deploy` when you're happy.
---
## When to use which pattern
| Pattern | What it runs | When to use |
|---------|--------------|-------------|
| **0. Quickstart** (default) | `python -m threadlight_quickstart` → MAF `Agent + SkillsProvider` + JSON stub tools + Streamlit UI on `localhost:8501` | **First reach-for after `threadlight-design`.** Closes the design → screen-shareable demo loop to <30 min. Zero Docker, zero MCP server boot, one LLM dep (Foundry project OR AOAI deployment OR GitHub Models via `GITHUB_TOKEN`). |
| **1. MCP-direct** (CLI ↔ MCP) | Just the PoC's FastMCP server on `localhost:8000`; CLI calls tools natively | You're iterating on **MCP tool implementation** (DB queries, business rules, error handling). The CLI itself is the agent. |
| **2. Smoke-client** (CLI → Python → Agent) | The PoC's `Agent + FoundryChatClient` invoked via `agent.run_async()` from a smoke script | You're iterating on the **prompt** or the **agent's tool-orchestration** behaviour. Skips the `ResponsesHostServer` HTTP layer. |
| **project-tools** (offline, no LLM) | `scripts/project_tools.py` imports the generated `src/mcp/server.py` and calls its REAL SPEC § 6 tools with the SPEC sample-data IDs | Acceptance for a [generate-only](../threadlight-deploy/SKILL.md#generate-only-mode) repo, or any repo whose MCP server came from `threadlight-deploy`. Proves the project's own tools work over its own data. It is not Pattern 0 generic CRUD. |
| **3. Local-stack** (compose) ⚠️ | All of: MCP server + Cosmos emulator + workspace UI on nginx + (optional) Search mock | End-to-end smoke before redeploying. **Linux / Windows x86 only** — Cosmos emulator container is fragile on macOS ARM; use Pattern 0 there. |
Reach for **Pattern 0** first. Drop to **Pattern 1** when you need
to iterate on the real MCP server. Use **Pattern 2** for headless
prompt tuning that needs to be driven from the CLI. **Pattern 3** is
a pre-deploy parity check on platforms where the Cosmos emulator
container actually works.
---
## Project-tools harness (generate-only acceptance)
`scripts/project_tools.py` is the offline harness for a repository produced by
`threadlight-deploy` generate-only mode. It needs no LLM, no login and no
network.
```bash
python3 skills/threadlight-local-test/scripts/project_tools.py \
--project <workspace> --report <workspace>/tests/project-tools-report.json
```
- It imports the generated `src/mcp/server.py` with `SAMPLE_DATA_DIR` pointing
at `specs/sample-data/`, so the calls go to the **project's real tools**.
- The call plan comes from `specs/project-tools.json`, which records the SPEC
§ 6 tools in order. Arguments are filled first from earlier results (for
example the `customer_id` returned by `get_order`), then from IDs found in
the sample data. SPEC inputs written as prose (`case + order + amount`)
become optional arguments (`case_id`, `order_id`, `amount`), which are sent
only when a real value is known.
- Every tool in the plan is called, with up to 3 argument candidates. A tool
that raises or returns an `error` result on every attempt fails the run.
- Contract failures fail the run before any call (`attempts: 0`): a tool with
no declared arguments, placeholder arguments such as `none_id`, or a
required `*_id` argument with no value in the sample data or earlier
results. The harness never sends `{}` (unless the SPEC explicitly declares
the tool's inputs as `none`) or an invented ID; required non-ID
values fall back to `"sample"`.
- It runs with bytecode writing disabled, so the project gains no
`__pycache__`.
- Report: `threadlight-local-test-report/v1` with `pattern: "project-tools"`.
Exit codes: 0 pass, 1 a tool failed, 2 no generated MCP server.
This is not Pattern 0: Pattern 0 boots generic CRUD stubs and an LLM. The
project-tools harness checks the tools that will actually ship.
---
## Pattern 0 — Quickstart (default)
The new default for "I just finished `threadlight-design`, now I want
to see it run." Consumes what design already emits — `specs/sample-data/`,
`src/agent/skills/<name>/SKILL.md` — and gives you a MAF agent + a
chat UI on `localhost:8501` without ever touching Docker, the MCP
server, or a real Cosmos.
> **Kratos-export mode.** Pattern 0 also boots a **Kratos-exported project**
> (`src/hosted-agent/` + `use-cases/<x>/` — see
> [`docs/KRATOS-BRIDGE.md`](../../docs/KRATOS-BRIDGE.md)) — the hosted-agent
> contract is identical, so the same MAF `Agent + SkillsProvider` recipe applies.
> Two path differences: load skills from the resolved skills root
> `use-cases/<x>/skills/` (auto-detected; `--skills-root` to override) instead of
> `src/agent/skills/`, and seed the in-memory store from the export's `mocks/`
> directory in place of `specs/sample-data/`. The agent identity/system prompt
> comes from `use-cases/<x>/SYSTEM_PROMPT.md`. No `azd up`, same fast loop.
### What you need
| Need | Why | One-time? |
|------|-----|-----------|
| **Python ≥ 3.10** + `pip` (or `uv`) | Run the reference package | Yes |
| **A threadlight-designed PoC** (the cwd at minimum has `specs/sample-data/*.json`) | Pattern 0 auto-discovers it | Per PoC |
| **One LLM endpoint** — Foundry project URL **OR** Azure OpenAI deployment **OR** GitHub Models (zero Azure) | The only external dep | Per tenant (shared GBB sandbox is fine; or just `gh auth token` for GitHub Models) |
| **Auth** — `az login` to the LLM tenant (Foundry/AOAI) **OR** `GITHUB_TOKEN` (GitHub Models) | Credential for the LLM call | Per tenant |
> **No Docker. No Cosmos emulator. No MCP server boot.** Pattern 0
> replaces all three with an in-memory dict-of-records loaded from
> `specs/sample-data/<entity>.json` and three CRUD tools per entity
> (`list_<entity>`, `get_<entity>`, `update_<entity>`).
### The bootstrap (one-time per PoC)
```bash
# 1) Install the quickstart package once (editable, from the catalog)
pip install -e <awesome-gbb>/skills/threadlight-local-test/references/quickstart
# 2) Drop the env template into the PoC and edit it
cp <awesome-gbb>/skills/threadlight-local-test/references/quickstart/.env.local.example .env.local
$EDITOR .env.local # set LLM_BACKEND: foundry (default), aoai, or copilot (GitHub Models, no Azure)
echo .env.local >> .gitignore
# 3) Sanity-check the wiring without a live LLM round-trip (<5s)
python -m threadlight_quickstart --check
```
### The loop (every time)
```bash
az login --tenant <dev-tid> # per threadlight-tenant-isolation skill
python -m threadlight_quickstart # Streamlit on http://localhost:8501
```
Or with the demo-prompt pump pre-loaded so you can step through the
prep-guide acts hands-free:
```bash
python -m threadlight_quickstart --simulator
```
`--simulator` reads prompts in priority order from:
1. `<poc-root>/tests/demo-prompts.txt` (one prompt per line; `#` comments allowed)
2. `<poc-root>/specs/prep-guide.html` § *Demo Script* (regex on `<strong>Type this:</strong>` blocks)
> **`.env.local` is auto-loaded.** The CLI parses `<poc-root>/.env.local`
> on every launch and injects the keys into the process env (only ones
> not already set in the shell). No need to `source` it manually.
> Disable with `THREADLIGHT_QUICKSTART_NO_TRANSCRIPT=1` if you don't
> want the side-effect.
> **Every UI turn appends to `<poc-root>/tests/quickstart.jsonl`.**
> Shape matches what [`microsoft-foundry`](https://github.com/microsoft/azure-skills/tree/v1.2.79/.github/plugins/azure-skills/skills/microsoft-foundry)
> consumes — `{ts, query, response}` per row. Run a few Pattern 0
> demos, then promote the JSONL into your Foundry eval dataset
> without reshaping. Disable with `THREADLIGHT_QUICKSTART_NO_TRANSCRIPT=1`.
> Add `tests/quickstart.jsonl` to your PoC's `.gitignore` (it's local
> demo state, not a fixture).
### How the tools come from your SPEC
For every `specs/sample-data/<entity>.json` discovered, Pattern 0
registers three MAF `@tool`-decorated callables backed by an
in-memory `InMemoryStore`:
| Tool | Returns | Notes |
|------|---------|-------|
| `list_<entity>(filters=None)` | `list[dict]` | Equality match on each field in the filters object; no filter → all records |
| `get_<entity>(id)` | `dict \| None` | Lookup by record id |
| `update_<entity>(id, fields)` | `dict` | Explicit fields object; mutates the in-memory snapshot; **reset every launch** |
The named object parameters are intentional: the actual native SDK schema omits
arbitrary `**kwargs`, which otherwise silently drops filters and update fields.
Existing Python store methods retain their keyword API; native tool callers use
the explicit `filters` / `fields` objects.
The agent's `SkillsProvider` discovers `src/agent/skills/<name>/SKILL.md`
under `from_paths(skills_dir)` and loads bodies on demand. Follow the
[autonomous MAF skill-read contract](../_shared/maf-skill-approval.md):
only trusted local instructions/resources bypass the provider's read approval;
scripts and other tools keep their policies. Unresolved native approvals raise
`approval_required`, surfaced by the existing UI error path rather than an empty
successful turn. This local configuration is not proof of hosted parity.
### Custom tools (when CRUD isn't enough)
For cross-entity joins, derived fields, business rules — drop a
`tests/quickstart_tools.py` next to the PoC, exposing:
```python
def register(tools: list, stores: dict) -> list:
"""Return extra MAF tools to append to the auto-generated CRUD set."""
from agent_framework import tool
@tool
def reassign_urgent(assignee: str) -> int:
urgent = stores["tickets"].list_all(severity="urgent")
for row in urgent:
stores["tickets"].update(row["id"], assignee=assignee)
return len(urgent)
return [reassign_urgent]
```
`agent_wiring` auto-discovers and calls `register(tools, stores)` after
the CRUD triple is built.
### What ships in the reference package
```
references/quickstart/
├── pyproject.toml # pip-installable; pins streamlit + agent-framework
├── threadlight_quickstart/
│ ├── __main__.py # `python -m threadlight_quickstart`
│ ├── cli.py # argparse: --check, --info, --simulator, --port
│ ├── discover.py # walks up cwd for the canonical PoC layout
│ ├── agent_wiring.py # Agent + SkillsProvider + tool registration
│ ├── stub_tools.py # InMemoryStore + CRUD tool factory
│ ├── simulator.py # demo-prompt cursor
│ └── ui_streamlit.py # the Streamlit chat page
├── .env.local.example # env-var template
├── Makefile.demo # drop-in for PoCs that want `make demo`
├── fixture-poc/ # 1-skill, 1-entity toy PoC (smoke target)
└── tests/ # pytest against fixture-poc
```
### When Pattern 0 is **not** the right answer
- **You need the real React workspace UI render** → Pattern 1 + run
the PoC's own `npm run dev:workspace` separately. Pattern 0 ships
Streamlit only on purpose (zero Node toolchain).
- **You're debugging the real MCP server** → Pattern 1. Pattern 0
bypasses the MCP layer entirely.
- **You need real Cosmos / Search semantics** (ranking, partition keys,
RU shape) → Pattern 3 on Linux/Windows x86, or `azd up` to a dev sub.
- **You need multi-agent orchestration** → Pattern 2.
---
## Prerequisites (one-time, dev box)
| Need | Why | Install |
|------|-----|---------|
| **Python 3.13** + `uv` | Run agent/MCP code | [uv install](https://docs.astral.sh/uv/) |
| **Docker Desktop** (or Rancher) | Cosmos emulator + nginx | Standard install |
GitHubで見る