@rubytech/create-maxy-code 0.1.68 → 0.1.70
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/package.json +1 -1
- package/payload/platform/config/brand.json +1 -0
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +163 -102
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +127 -13
- package/payload/platform/plugins/docs/references/admin-session.md +22 -18
- package/payload/platform/plugins/docs/references/platform.md +4 -0
- package/payload/platform/plugins/memory/PLUGIN.md +9 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js +116 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +100 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js +197 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts +33 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js +44 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +29 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +57 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts +9 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js +36 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +7 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +22 -11
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +2 -2
- package/payload/platform/scripts/lib/read-brand-json.sh +69 -0
- package/payload/platform/scripts/seed-neo4j.sh +67 -8
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +73 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +8 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +49 -4
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +11 -3
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +45 -20
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +7 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +5 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +4 -67
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/brand-design/SKILL.md +48 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/make-brochure/SKILL.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +2 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +284 -11
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/index.html +1 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/registers.md +66 -8
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/template.html +4 -1
- package/payload/server/public/assets/{admin-BPRjc4z9.js → admin-Bej-Q1IZ.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Asw7tNG5.js → architectureDiagram-Q4EWVU46-umb1qQ5e.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-Blvay4_b.js → blockDiagram-DXYQGD6D-C8kHw50g.js} +1 -1
- package/payload/server/public/assets/{brand-CwHvVF8s.css → brand-DxX912hX.css} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-C2st6jZP.js → c4Diagram-AHTNJAMY-CtKxKvPu.js} +1 -1
- package/payload/server/public/assets/channel-BshbIIpE.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-Cp7B2yh1.js → chunk-336JU56O-_xUA1Yj-.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-B0x3RXvj.js → chunk-426QAEUC-d_A8vQqM.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-B0yM0pgq.js → chunk-4TB4RGXK-BgkVBpN_.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-CqGhfArO.js → chunk-5FUZZQ4R-CGOqTZDS.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-jSeD3Yrj.js → chunk-5PVQY5BW-CW78oYeu.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-C7HuvG2r.js → chunk-EDXVE4YY-D_w_ZcbQ.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-DzCrge6H.js → chunk-ENJZ2VHE-MIcdFlwQ.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-BPvCh0Yc.js → chunk-ICPOFSXX-BaRHp6jX.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-DamxwrTn.js → chunk-OYMX7WX6-Cgo3QttE.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-DDiVd0qV.js → chunk-U2HBQHQK-BVZMJI1z.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-CDof681v.js → chunk-X2U36JSP-DdgOyRDq.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-DYzaYjrN.js → chunk-YZCP3GAM-CO61zu06.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-j0bcHHzU.js → chunk-ZZ45TVLE-B3BNOUKS.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-mzmkY4eP.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Co3bBdih.js +1 -0
- package/payload/server/public/assets/clone-BRSMOear.js +1 -0
- package/payload/server/public/assets/{dagre-C8nbdG6y.js → dagre-BY0FcwR1.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-1RnGN_fy.js → dagre-KV5264BT-YvKOR5T0.js} +1 -1
- package/payload/server/public/assets/data-LkSd6fEQ.js +1 -0
- package/payload/server/public/assets/{device-url-actions-C_Keyr0L.js → device-url-actions-Cq5VIBKc.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-C6d3Rrui.js → diagram-5BDNPKRD-DL3SiNlA.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-aD4Dik01.js → diagram-G4DWMVQ6-C_IwINPH.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-CAub49YE.js → diagram-MMDJMWI5-vumL8N8U.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-CAKnAfEr.js → diagram-TYMM5635-ewBaf58r.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-BrqOworC.js → erDiagram-SMLLAGMA-kiidRkW3.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-DV4eU-T2.js → flowDiagram-DWJPFMVM-C-1rI_Iw.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DayXUN42.js → ganttDiagram-T4ZO3ILL-B_9YdwWh.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CK1xV1OV.js → gitGraphDiagram-UUTBAWPF-BmwivVe3.js} +1 -1
- package/payload/server/public/assets/graph-CPXz4A4I.js +1 -0
- package/payload/server/public/assets/graph-labels-CZR9j5Ge.js +1 -0
- package/payload/server/public/assets/{graphlib-CkF9o1XT.js → graphlib-Dvt3mNtm.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-CiepM2H6.js → infoDiagram-42DDH7IO-B2n7mCmp.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DwNkdQ6z.js → ishikawaDiagram-UXIWVN3A-C2ejJadN.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-BlDLMfu_.js → journeyDiagram-VCZTEJTY-BaIXYVwB.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-ayZTc0Oj.js → kanban-definition-6JOO6SKY-B1NCV7hH.js} +1 -1
- package/payload/server/public/assets/{line-LhPUiuH-.js → line-BEjGPKWc.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-DyZYsQv_.js → mermaid-parser.core-DwLM_Bkq.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-CauxgRdz.js → mermaid.core-BwedTSna.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CQzcaJuh.js → mindmap-definition-QFDTVHPH-D6q_XhF7.js} +1 -1
- package/payload/server/public/assets/{page-DAw3-U_s.js → page-BBRZF8Z8.js} +1 -1
- package/payload/server/public/assets/{page-BGv81dGN.js → page-CLr-z4E2.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-BPhAeTm9.js → pieDiagram-DEJITSTG-CAKQ-eUh.js} +1 -1
- package/payload/server/public/assets/{public-CGI24wrL.js → public-CfVPuA1x.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CxrzL3ZF.js → quadrantDiagram-34T5L4WZ-B8_Xl5p8.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-ZBSC4fGX.js → requirementDiagram-MS252O5E-BWSq9eFs.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-BTDBEisC.js → sankeyDiagram-XADWPNL6-V3uWSRXp.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-Bv7fA5_J.js → sequenceDiagram-FGHM5R23-DJ_F3-vT.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-D07lcZrk.js → stateDiagram-FHFEXIEX-odlwuCTP.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DE5qFFDY.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DyeyUqKa.js → timeline-definition-GMOUNBTQ-DXE7m_zC.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-B9FqDmwS.js → vennDiagram-DHZGUBPP-DEiEepoV.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-4-5uCIAD.js → wardleyDiagram-NUSXRM2D-CRhVx95h.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-WdkP5T_y.js → xychartDiagram-5P7HB3ND-9Gv4VNaG.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +5 -5
- package/payload/server/public/index.html +7 -7
- package/payload/server/public/public.html +4 -4
- package/payload/server/public/assets/channel-DTh_uFbz.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-Dsupydrn.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DrwAXnBf.js +0 -1
- package/payload/server/public/assets/clone-COx5mf-H.js +0 -1
- package/payload/server/public/assets/data-DOI26n9p.js +0 -1
- package/payload/server/public/assets/graph-DP9P7TKZ.js +0 -1
- package/payload/server/public/assets/graph-labels-BoLyECfJ.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Cr1Qet-f.js +0 -1
- /package/payload/server/public/assets/{brand-B8JhGW9e.js → brand-BT-mNX7-.js} +0 -0
package/package.json
CHANGED
|
@@ -129,7 +129,7 @@ Tools are available via the `admin` MCP server.
|
|
|
129
129
|
- `hooks/pre-tool-use.sh` — enforces admin agent write boundaries
|
|
130
130
|
- `hooks/playwright-file-guard.sh` — rewrites file:// URLs to a backgrounded loopback http.server before Playwright sees them
|
|
131
131
|
- `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
|
|
132
|
-
- `hooks/turn-completed-graph-write.sh` — Stop hook fired once per completed admin-agent turn. Gates on `MAXY_SESSION_ROLE=admin` + `MAXY_SPECIALIST!=database-operator` so it never recurses into the recorder PTY or fires on public sessions. **Task 147** — the recorder is spawned via the same route Sidebar uses. ONE POST to `POST /api/admin/claude-sessions`, body carries `{specialist:'database-operator', model:'haiku', initialMessage:<json-envelope>, adminSessionId:<op>}` — no synthetic `senderId: 'turn-recorder'` marker. The Hono wrapper bypasses cookie auth on this exact method+path when the request originates from `127.0.0.1` (same trust boundary the claude-session-manager itself relies on), looks up the operator's senderId from the manager's `/<adminSessionId>/meta`, and forwards a Sidebar-shape spawn body. The `/recorder-spawn` sibling route is gone. The hook reads its UI port from `MAXY_UI_INTERNAL_PORT` (stamped on the manager systemd unit) — no fallback; absence emits `[turn-recorder] spawn-failed reason=missing-env env=MAXY_UI_INTERNAL_PORT` to stderr instead of silently 19199-ing. **Task 177** — `initialMessage` is a JSON-stringified envelope
|
|
132
|
+
- `hooks/turn-completed-graph-write.sh` — Stop hook fired once per completed admin-agent turn. Gates on `MAXY_SESSION_ROLE=admin` + `MAXY_SPECIALIST!=database-operator` so it never recurses into the recorder PTY or fires on public sessions. **Task 147** — the recorder is spawned via the same route Sidebar uses. ONE POST to `POST /api/admin/claude-sessions`, body carries `{specialist:'database-operator', model:'haiku', initialMessage:<json-envelope>, adminSessionId:<op>}` — no synthetic `senderId: 'turn-recorder'` marker. The Hono wrapper bypasses cookie auth on this exact method+path when the request originates from `127.0.0.1` (same trust boundary the claude-session-manager itself relies on), looks up the operator's senderId from the manager's `/<adminSessionId>/meta`, and forwards a Sidebar-shape spawn body. The `/recorder-spawn` sibling route is gone. The hook reads its UI port from `MAXY_UI_INTERNAL_PORT` (stamped on the manager systemd unit) — no fallback; absence emits `[turn-recorder] spawn-failed reason=missing-env env=MAXY_UI_INTERNAL_PORT` to stderr instead of silently 19199-ing. **Task 177** — `initialMessage` is a JSON-stringified envelope; the schema lives in [`platform/plugins/docs/references/admin-session.md`](../docs/references/admin-session.md) under "`initialMessage` JSON envelope (Task 177)". Top-level keys exactly: `turns`, `conversationId`, `accountId`, `occurredAt`. `turns` is the full conversation transcript, oldest first, newest last, with each entry `{ role: "user"|"assistant", text, ts, toolCalls? }`. No windowing, no truncation. Multi-record assistant messages collapse on `message.id`. `tool_use` and matching `tool_result` blocks attach as a single `toolCalls` entry on the owning assistant turn; the user record carrying only the `tool_result` does not create a separate user turn. No leading instruction prose. `toolCalls[].input` and `toolCalls[].output` are native JSON values, never re-stringified. (Replaces Task 175's `(operatorMessage, assistantReply)` pair, which asserted a temporal Q→A relationship the walker never enforced.) One observability line `[turn-recorder] envelope sessionId=<op> turnsCount=<n> userTurns=<n> assistantTurns=<n> toolCallTurns=<n>` precedes `spawn-request`. The envelope rides on the `/spawn` body as a trailing positional argv to `claude`, so the database-operator session's JSONL first `role=user` line is the JSON object verbatim. No separate `POST /:id/input` call, no bracketed-paste, no keystroke injection. The recorder-auto-archive subscriber stops the recorder PTY as soon as its JSONL contains an assistant message with `stop_reason === "end_turn"`. **Task 129** — every emit goes through `POST /api/admin/log-ingest` so the lines land in `server.log` keyed by the operator session id. The chain is `trigger` → `spawn-request` → `spawn-success` → `input-delivered` → `tool-call` × N → `tool-result` × N → `write-complete` → `auto-archive`; each gated-off path emits one `trigger-skipped reason=<role-not-admin|is-recorder|empty-stdin|missing-transcript|conversation-empty>` line. Failure modes — `spawn-failed`, `tool-surface-missing`, `input-failed`, `write-empty`, `auto-archive reason=stale-recorder` — each emit one named line; absence is itself a defect.
|
|
133
133
|
|
|
134
134
|
## Session identifiers (Task 135)
|
|
135
135
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Task 129/131/165/175/177 regression test for the Stop hook.
|
|
2
|
+
# Task 129/131/165/175/177/195 regression test for the Stop hook.
|
|
3
3
|
#
|
|
4
|
-
# Contract:
|
|
4
|
+
# Contract (post-195):
|
|
5
5
|
# - Every log emission goes through `POST /api/admin/log-ingest`. Hook
|
|
6
6
|
# stderr is silent on every path.
|
|
7
7
|
# - Every gated-off path emits one `trigger-skipped` line carrying a
|
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
# `conversationBytes=<n>` and exactly ONE POST to
|
|
12
12
|
# `/api/admin/claude-sessions`. The body carries the operator
|
|
13
13
|
# session id as `adminSessionId` and an `initialMessage` whose value
|
|
14
|
-
# is
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
14
|
+
# is the FILLED database-operator paragraph — the four-sentence
|
|
15
|
+
# template with `{schema}` replaced by `schema-base.md` (plus the
|
|
16
|
+
# brand-declared vertical when set) and `{conversation}` replaced by
|
|
17
|
+
# a flat `role: text` rendering of the envelope's `turns` array with
|
|
18
|
+
# inline `[tool: name(args)]` markers on assistant turns that called
|
|
19
|
+
# tools. The initialMessage is a plain string, not a JSON-stringified
|
|
20
|
+
# envelope; no instruction prose, no leading JSON.
|
|
21
|
+
# - The hook emits exactly one `[turn-recorder] envelope ...` line and
|
|
22
|
+
# one `[turn-recorder] substitution ...` line before `spawn-request`.
|
|
23
|
+
# The substitution line carries `schemaBytes`, `conversationBytes`,
|
|
24
|
+
# and `bodyBytes` so the per-spawn substitution is auditable.
|
|
22
25
|
# - The hook never writes the legacy `spawn-with-input` / `fired` stderr
|
|
23
26
|
# lines.
|
|
24
27
|
#
|
|
@@ -219,57 +222,88 @@ else
|
|
|
219
222
|
fail "case-5b expected exactly 1 recorder-spawn POST, got $RECORDER_LINES"
|
|
220
223
|
fi
|
|
221
224
|
|
|
222
|
-
# 5c.
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
225
|
+
# 5c. (Task 195) initialMessage is the FILLED four-sentence paragraph.
|
|
226
|
+
# The template's `{schema}` is replaced by schema-base.md content;
|
|
227
|
+
# `{conversation}` is replaced by `role: text` lines for every turn.
|
|
228
|
+
# The string is plain text, not a JSON envelope. The template's
|
|
229
|
+
# opening sentence and closing sentence are present verbatim; the
|
|
230
|
+
# four operator/assistant texts each appear on their own line.
|
|
226
231
|
RECORDER_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
227
232
|
BODY_OK=$(printf '%s' "$RECORDER_BODY" | python3 -c '
|
|
228
|
-
import sys, json
|
|
233
|
+
import sys, json
|
|
229
234
|
op = sys.argv[1]
|
|
230
|
-
acct = sys.argv[2]
|
|
231
235
|
try:
|
|
232
236
|
outer = json.load(sys.stdin)
|
|
233
|
-
|
|
234
|
-
if not isinstance(
|
|
235
|
-
print("no:initialMessage-not-string:"+repr(type(
|
|
237
|
+
msg = outer.get("initialMessage")
|
|
238
|
+
if not isinstance(msg, str):
|
|
239
|
+
print("no:initialMessage-not-string:"+repr(type(msg).__name__))
|
|
236
240
|
sys.exit(0)
|
|
237
|
-
msg = json.loads(msg_str)
|
|
238
|
-
keys = set(msg.keys())
|
|
239
|
-
turns = msg.get("turns") or []
|
|
240
241
|
conds = []
|
|
241
|
-
conds.append(("
|
|
242
|
-
|
|
243
|
-
conds.append(("
|
|
244
|
-
|
|
245
|
-
conds.append(("
|
|
246
|
-
conds.append(("no-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
conds.append(("t3-text", turns[3].get("text") == "One question to anchor the next move: what is your name?"))
|
|
242
|
+
conds.append(("starts-with-template",
|
|
243
|
+
msg.startswith("You are an expert Neo4J graph operator.")))
|
|
244
|
+
conds.append(("ends-with-template",
|
|
245
|
+
msg.rstrip().endswith("do not emit it.")))
|
|
246
|
+
conds.append(("no-placeholder-schema", "{schema}" not in msg))
|
|
247
|
+
conds.append(("no-placeholder-conversation", "{conversation}" not in msg))
|
|
248
|
+
# schema-base.md content is interpolated.
|
|
249
|
+
conds.append(("contains-schema-base-header", "Schema Reference" in msg))
|
|
250
|
+
# The conversation lines appear in role: text format.
|
|
251
|
+
conds.append(("has-user-1", "\nuser: New Real Agent session" in msg))
|
|
252
|
+
conds.append(("has-asst-1",
|
|
253
|
+
"\nassistant: Session noted. What would you like to work on?" in msg))
|
|
254
|
+
conds.append(("has-user-2", "\nuser: you tell me" in msg))
|
|
255
|
+
conds.append(("has-asst-2",
|
|
256
|
+
"\nassistant: One question to anchor the next move: what is your name?"
|
|
257
|
+
in msg))
|
|
258
|
+
conds.append(("no-leading-json", not msg.lstrip().startswith("{")))
|
|
259
|
+
conds.append(("no-operatorMessage", "operatorMessage" not in msg))
|
|
260
|
+
conds.append(("no-assistantReply", "assistantReply" not in msg))
|
|
261
261
|
conds.append(("outer-specialist", outer.get("specialist") == "database-operator"))
|
|
262
262
|
conds.append(("outer-model", outer.get("model") == "haiku"))
|
|
263
263
|
conds.append(("outer-adminSessionId", outer.get("adminSessionId") == op))
|
|
264
|
+
conds.append(("outer-channel", outer.get("channel") == "browser"))
|
|
264
265
|
failed = [name for name, ok in conds if not ok]
|
|
265
|
-
print("yes" if not failed else "no:"+repr(failed)
|
|
266
|
+
print("yes" if not failed else "no:"+repr(failed))
|
|
266
267
|
except Exception as e:
|
|
267
268
|
print("parse-fail:"+str(e))
|
|
268
|
-
' "$OP_ID"
|
|
269
|
+
' "$OP_ID" 2>/dev/null)
|
|
269
270
|
if [[ "$BODY_OK" == "yes" ]]; then
|
|
270
|
-
pass "case-5c
|
|
271
|
+
pass "case-5c initialMessage is the filled four-sentence paragraph; placeholders substituted; transcript rendered as role: text lines"
|
|
272
|
+
else
|
|
273
|
+
fail "case-5c filled-prompt shape wrong ($BODY_OK)"
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
# 5h. (Task 195) exactly one `substitution` log-ingest line carrying
|
|
277
|
+
# positive byte counts for schema / conversation / body.
|
|
278
|
+
SUBST_LINE_COUNT=$(ingest_lines | grep -cE "^substitution sessionId=${OP_ID} schemaBytes=[1-9][0-9]* conversationBytes=[1-9][0-9]* bodyBytes=[1-9][0-9]*$" || true)
|
|
279
|
+
if [[ "$SUBST_LINE_COUNT" -eq 1 ]]; then
|
|
280
|
+
pass "case-5h substitution log line emitted once with positive byte counts"
|
|
281
|
+
else
|
|
282
|
+
fail "case-5h expected exactly 1 substitution line w/ positive bytes, got $SUBST_LINE_COUNT (lines: $(ingest_lines))"
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
# 5i. (Task 195) substitution line lands between envelope and spawn-request.
|
|
286
|
+
ALL_5I_LINES=$(ingest_lines)
|
|
287
|
+
ENV_POS_5I=$(printf '%s\n' "$ALL_5I_LINES" | grep -nE "^envelope sessionId=${OP_ID}" | head -1 | cut -d: -f1)
|
|
288
|
+
SUB_POS_5I=$(printf '%s\n' "$ALL_5I_LINES" | grep -nE "^substitution sessionId=${OP_ID}" | head -1 | cut -d: -f1)
|
|
289
|
+
SPAWN_POS_5I=$(printf '%s\n' "$ALL_5I_LINES" | grep -nE "^spawn-request sessionId=${OP_ID}" | head -1 | cut -d: -f1)
|
|
290
|
+
if [[ -n "$ENV_POS_5I" && -n "$SUB_POS_5I" && -n "$SPAWN_POS_5I" \
|
|
291
|
+
&& "$ENV_POS_5I" -lt "$SUB_POS_5I" && "$SUB_POS_5I" -lt "$SPAWN_POS_5I" ]]; then
|
|
292
|
+
pass "case-5i envelope < substitution < spawn-request ordering preserved"
|
|
271
293
|
else
|
|
272
|
-
fail "case-
|
|
294
|
+
fail "case-5i ordering wrong (env=$ENV_POS_5I sub=$SUB_POS_5I spawn=$SPAWN_POS_5I)"
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# 5j. (Task 195) `initialMessageBytes` on spawn-request equals `bodyBytes`
|
|
298
|
+
# on substitution — the two log lines are reading the same number.
|
|
299
|
+
BODY_BYTES_5J=$(printf '%s\n' "$ALL_5I_LINES" | grep -E "^substitution sessionId=${OP_ID}" | head -1 \
|
|
300
|
+
| sed -E 's/.*bodyBytes=([0-9]+).*/\1/')
|
|
301
|
+
INIT_BYTES_5J=$(printf '%s\n' "$ALL_5I_LINES" | grep -E "^spawn-request sessionId=${OP_ID}" | head -1 \
|
|
302
|
+
| sed -E 's/.*initialMessageBytes=([0-9]+).*/\1/')
|
|
303
|
+
if [[ -n "$BODY_BYTES_5J" && "$BODY_BYTES_5J" == "$INIT_BYTES_5J" ]]; then
|
|
304
|
+
pass "case-5j substitution.bodyBytes == spawn-request.initialMessageBytes (=$BODY_BYTES_5J)"
|
|
305
|
+
else
|
|
306
|
+
fail "case-5j bodyBytes ($BODY_BYTES_5J) does not match initialMessageBytes ($INIT_BYTES_5J)"
|
|
273
307
|
fi
|
|
274
308
|
|
|
275
309
|
# 5d. exactly ONE `spawn-request` log-ingest line
|
|
@@ -307,12 +341,12 @@ else
|
|
|
307
341
|
pass "case-5g no legacy stderr emissions"
|
|
308
342
|
fi
|
|
309
343
|
|
|
310
|
-
# --- Case 6 (Task
|
|
311
|
-
# Two assistant
|
|
312
|
-
#
|
|
313
|
-
#
|
|
314
|
-
#
|
|
315
|
-
#
|
|
344
|
+
# --- Case 6 (Task 195): multi-block assistant collapse ----------------
|
|
345
|
+
# Two assistant records sharing `message.id`: thinking-only then text.
|
|
346
|
+
# The walker collapses both into ONE assistant turn whose text is the
|
|
347
|
+
# text-record's text. The filled prompt's conversation portion has
|
|
348
|
+
# exactly two transcript lines: `user: think then answer` and
|
|
349
|
+
# `assistant: answer is 42`.
|
|
316
350
|
MULTI_TRANSCRIPT=$(mktemp); TMPFILES+=("$MULTI_TRANSCRIPT")
|
|
317
351
|
{
|
|
318
352
|
printf '{"type":"user","timestamp":"2026-05-19T21:30:00.000Z","message":{"role":"user","content":"think then answer"}}\n'
|
|
@@ -331,23 +365,31 @@ MULTI_OK=$(printf '%s' "$MULTI_BODY" | python3 -c '
|
|
|
331
365
|
import sys, json
|
|
332
366
|
try:
|
|
333
367
|
outer = json.load(sys.stdin)
|
|
334
|
-
msg =
|
|
335
|
-
|
|
368
|
+
msg = outer.get("initialMessage")
|
|
369
|
+
if not isinstance(msg, str):
|
|
370
|
+
print("no:initialMessage-not-string")
|
|
371
|
+
sys.exit(0)
|
|
336
372
|
conds = []
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
373
|
+
# Filled-prompt sanity.
|
|
374
|
+
conds.append(("starts-template", msg.startswith("You are an expert Neo4J graph operator.")))
|
|
375
|
+
conds.append(("no-placeholder-conv", "{conversation}" not in msg))
|
|
376
|
+
# Conversation lines.
|
|
377
|
+
conds.append(("has-user", "\nuser: think then answer" in msg))
|
|
378
|
+
conds.append(("has-asst", "\nassistant: answer is 42" in msg))
|
|
379
|
+
# Thinking content does NOT bleed in.
|
|
380
|
+
conds.append(("no-thinking-text", "considering..." not in msg))
|
|
381
|
+
# Exactly two transcript lines (one per turn) — no duplicate assistant.
|
|
382
|
+
user_lines = msg.count("\nuser: ")
|
|
383
|
+
asst_lines = msg.count("\nassistant: ")
|
|
384
|
+
conds.append(("one-user-line", user_lines == 1))
|
|
385
|
+
conds.append(("one-asst-line", asst_lines == 1))
|
|
344
386
|
failed = [name for name, ok in conds if not ok]
|
|
345
|
-
print("yes" if not failed else "no:"+repr(failed)
|
|
387
|
+
print("yes" if not failed else "no:"+repr(failed))
|
|
346
388
|
except Exception as e:
|
|
347
389
|
print("parse-fail:"+str(e))
|
|
348
390
|
' 2>/dev/null)
|
|
349
391
|
if [[ "$MULTI_OK" == "yes" ]]; then
|
|
350
|
-
pass "case-6 multi-block assistant collapses to one
|
|
392
|
+
pass "case-6 multi-block assistant collapses to one transcript line; thinking content does not bleed in"
|
|
351
393
|
else
|
|
352
394
|
fail "case-6 multi-block collapse wrong ($MULTI_OK): $MULTI_BODY"
|
|
353
395
|
fi
|
|
@@ -387,30 +429,35 @@ TOOL_OK=$(printf '%s' "$TOOL_BODY" | python3 -c '
|
|
|
387
429
|
import sys, json
|
|
388
430
|
try:
|
|
389
431
|
outer = json.load(sys.stdin)
|
|
390
|
-
msg =
|
|
391
|
-
|
|
432
|
+
msg = outer.get("initialMessage")
|
|
433
|
+
if not isinstance(msg, str):
|
|
434
|
+
print("no:initialMessage-not-string")
|
|
435
|
+
sys.exit(0)
|
|
392
436
|
conds = []
|
|
393
|
-
#
|
|
394
|
-
#
|
|
395
|
-
conds.append(("
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
437
|
+
# The user turn carrying only a tool_result becomes no transcript line;
|
|
438
|
+
# so the filled prompt should have exactly ONE `user:` line.
|
|
439
|
+
conds.append(("one-user-line", msg.count("\nuser: ") == 1))
|
|
440
|
+
conds.append(("has-user-prompt", "\nuser: please write a node" in msg))
|
|
441
|
+
# Two assistant lines: one carrying the tool_use marker (with empty
|
|
442
|
+
# text body), one carrying "done".
|
|
443
|
+
conds.append(("two-asst-lines", msg.count("\nassistant: ") == 2))
|
|
444
|
+
conds.append(("has-asst-done", "\nassistant: done" in msg))
|
|
445
|
+
# Tool-call marker on the owning assistant turn — name and full JSON
|
|
446
|
+
# input round-trip inline.
|
|
447
|
+
conds.append(("has-tool-marker", "[tool: mcp__memory__memory-write(" in msg))
|
|
448
|
+
conds.append(("tool-marker-name", "\"name\": \"Smalleys\"" in msg or "\"name\":\"Smalleys\"" in msg))
|
|
449
|
+
conds.append(("tool-marker-nested", "\"nested\"" in msg))
|
|
450
|
+
conds.append(("tool-marker-nodeType", "\"nodeType\": \"LocalBusiness\"" in msg or "\"nodeType\":\"LocalBusiness\"" in msg))
|
|
451
|
+
# The tool_result content does NOT bleed in (the rendering chose
|
|
452
|
+
# input-only markers; output goes nowhere in the filled prompt).
|
|
453
|
+
conds.append(("no-output-leakage", "ok: wrote LocalBusiness" not in msg))
|
|
407
454
|
failed = [name for name, ok in conds if not ok]
|
|
408
|
-
print("yes" if not failed else "no:"+repr(failed)
|
|
455
|
+
print("yes" if not failed else "no:"+repr(failed))
|
|
409
456
|
except Exception as e:
|
|
410
457
|
print("parse-fail:"+str(e))
|
|
411
458
|
' 2>/dev/null)
|
|
412
459
|
if [[ "$TOOL_OK" == "yes" ]]; then
|
|
413
|
-
pass "case-7 tool_use
|
|
460
|
+
pass "case-7 assistant tool_use renders as [tool: name(args)] marker on the owning turn; tool_result does not create a separate user line"
|
|
414
461
|
else
|
|
415
462
|
fail "case-7 tool pairing wrong ($TOOL_OK): $TOOL_BODY"
|
|
416
463
|
fi
|
|
@@ -449,23 +496,29 @@ LONG_OK=$(printf '%s' "$LONG_BODY" | python3 -c '
|
|
|
449
496
|
import sys, json
|
|
450
497
|
try:
|
|
451
498
|
outer = json.load(sys.stdin)
|
|
452
|
-
msg =
|
|
453
|
-
|
|
499
|
+
msg = outer.get("initialMessage")
|
|
500
|
+
if not isinstance(msg, str):
|
|
501
|
+
print("no:initialMessage-not-string")
|
|
502
|
+
sys.exit(0)
|
|
503
|
+
user_lines = msg.count("\nuser: ")
|
|
504
|
+
asst_lines = msg.count("\nassistant: ")
|
|
454
505
|
conds = []
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
506
|
+
# No windowing — every one of the 100 turns appears as a transcript line.
|
|
507
|
+
conds.append(("100-user", user_lines == 50))
|
|
508
|
+
conds.append(("100-asst", asst_lines == 50))
|
|
509
|
+
conds.append(("first-user-0", "\nuser: user msg 0" in msg))
|
|
510
|
+
conds.append(("last-user-49", "\nuser: user msg 49" in msg))
|
|
511
|
+
conds.append(("last-asst-49", "\nassistant: asst reply 49" in msg))
|
|
512
|
+
# The filled prompt is large — schema + 100 transcript lines easily
|
|
513
|
+
# exceeds 5_000 bytes even with a tiny schema.
|
|
514
|
+
conds.append(("body-large", len(msg.encode("utf-8")) > 5000))
|
|
462
515
|
failed = [name for name, ok in conds if not ok]
|
|
463
|
-
print("yes" if not failed else "no:"+repr(failed)+"|
|
|
516
|
+
print("yes" if not failed else "no:"+repr(failed)+"|users="+str(user_lines)+"|assts="+str(asst_lines))
|
|
464
517
|
except Exception as e:
|
|
465
518
|
print("parse-fail:"+str(e))
|
|
466
519
|
' 2>/dev/null)
|
|
467
520
|
if [[ "$LONG_OK" == "yes" ]]; then
|
|
468
|
-
pass "case-8 100-turn transcript →
|
|
521
|
+
pass "case-8 100-turn transcript → 50 user lines + 50 assistant lines in filled prompt; no truncation"
|
|
469
522
|
else
|
|
470
523
|
fail "case-8 long-conversation wrong ($LONG_OK)"
|
|
471
524
|
fi
|
|
@@ -522,24 +575,32 @@ run_hook "admin" "" "$ESC_ENVELOPE"
|
|
|
522
575
|
ESC_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
523
576
|
ESC_OK=$(printf '%s' "$ESC_BODY" | python3 -c '
|
|
524
577
|
import sys, json
|
|
525
|
-
|
|
526
|
-
|
|
578
|
+
# The user turn text contains literal quotes, a backslash-n (two chars),
|
|
579
|
+
# a real newline, an em-dash, and a fox emoji. The assistant text has
|
|
580
|
+
# two backslashes and a real newline.
|
|
581
|
+
EXPECTED_OP_FRAG_1 = "hello \"world\""
|
|
582
|
+
EXPECTED_OP_FRAG_2 = "last — café \U0001F98A"
|
|
583
|
+
EXPECTED_ASST_FRAG = "reply with backslash \\\\ and newline"
|
|
527
584
|
try:
|
|
528
585
|
outer = json.load(sys.stdin)
|
|
529
|
-
msg =
|
|
530
|
-
|
|
586
|
+
msg = outer.get("initialMessage")
|
|
587
|
+
if not isinstance(msg, str):
|
|
588
|
+
print("no:initialMessage-not-string")
|
|
589
|
+
sys.exit(0)
|
|
531
590
|
conds = []
|
|
532
|
-
conds.append(("
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
591
|
+
conds.append(("op-quotes", EXPECTED_OP_FRAG_1 in msg))
|
|
592
|
+
conds.append(("op-emoji", EXPECTED_OP_FRAG_2 in msg))
|
|
593
|
+
conds.append(("asst-backslash", EXPECTED_ASST_FRAG in msg))
|
|
594
|
+
conds.append(("user-prefix", "\nuser: " in msg))
|
|
595
|
+
conds.append(("asst-prefix", "\nassistant: " in msg))
|
|
596
|
+
conds.append(("template-intact", "You are an expert Neo4J graph operator." in msg))
|
|
536
597
|
failed = [name for name, ok in conds if not ok]
|
|
537
|
-
print("yes" if not failed else "no:"+repr(failed)
|
|
598
|
+
print("yes" if not failed else "no:"+repr(failed))
|
|
538
599
|
except Exception as e:
|
|
539
600
|
print("parse-fail:"+str(e))
|
|
540
601
|
' 2>/dev/null)
|
|
541
602
|
if [[ "$ESC_OK" == "yes" ]]; then
|
|
542
|
-
pass "case-10 escaping: quotes/backslashes/
|
|
603
|
+
pass "case-10 escaping: quotes / backslashes / em-dash / emoji survive interpolation into the filled prompt"
|
|
543
604
|
else
|
|
544
605
|
fail "case-10 escaping wrong ($ESC_OK): $ESC_BODY"
|
|
545
606
|
fi
|
|
@@ -283,26 +283,140 @@ print(f"{len(turns)} {u} {a} {tc}")
|
|
|
283
283
|
read -r TURNS_COUNT USER_TURNS ASST_TURNS TC_TURNS <<<"$ENVELOPE_COUNTS"
|
|
284
284
|
emit_log "envelope sessionId=${ADMIN_SESSION_ID} turnsCount=${TURNS_COUNT} userTurns=${USER_TURNS} assistantTurns=${ASST_TURNS} toolCallTurns=${TC_TURNS}"
|
|
285
285
|
|
|
286
|
-
#
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
# the
|
|
290
|
-
|
|
286
|
+
# Task 195 — substitute {schema} and {conversation} placeholders in the
|
|
287
|
+
# database-operator agent body before spawning. The agent file on disk
|
|
288
|
+
# carries the four-sentence template verbatim with both placeholders; the
|
|
289
|
+
# hook renders the actual schema text and a flat `role: text` conversation
|
|
290
|
+
# transcript here, then sends the filled paragraph as `initialMessage`.
|
|
291
|
+
# The model's first input is the filled prompt — no JSON envelope, no
|
|
292
|
+
# instruction prose, no placeholders.
|
|
293
|
+
HOOK_DIR_RESOLVED="$(cd "$(dirname "$0")" && pwd)"
|
|
294
|
+
PLATFORM_ROOT_RESOLVED="$(cd "${HOOK_DIR_RESOLVED}/../../.." && pwd)"
|
|
295
|
+
SCHEMA_BASE_PATH="${PLATFORM_ROOT_RESOLVED}/plugins/memory/references/schema-base.md"
|
|
296
|
+
BRAND_JSON_PATH="${PLATFORM_ROOT_RESOLVED}/config/brand.json"
|
|
297
|
+
AGENT_BODY_PATH="${PLATFORM_ROOT_RESOLVED}/templates/specialists/agents/database-operator.md"
|
|
298
|
+
|
|
299
|
+
if [ ! -f "$SCHEMA_BASE_PATH" ]; then
|
|
300
|
+
emit_log "spawn-failed sessionId=${ADMIN_SESSION_ID} reason=schema-base-missing path=${SCHEMA_BASE_PATH}"
|
|
301
|
+
exit 0
|
|
302
|
+
fi
|
|
303
|
+
if [ ! -f "$AGENT_BODY_PATH" ]; then
|
|
304
|
+
emit_log "spawn-failed sessionId=${ADMIN_SESSION_ID} reason=agent-body-missing path=${AGENT_BODY_PATH}"
|
|
305
|
+
exit 0
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
# Brand-declared vertical (Task 193). Treated as null when brand.json is
|
|
309
|
+
# absent, unparseable, lacks the field, or names a file that does not
|
|
310
|
+
# exist — every one of those collapses to "schema-base only", never
|
|
311
|
+
# loud-fails this hook. Task 193 itself is the loud-fail surface for
|
|
312
|
+
# brand-config defects.
|
|
313
|
+
VERTICAL_PATH=""
|
|
314
|
+
if [ -f "$BRAND_JSON_PATH" ]; then
|
|
315
|
+
VERTICAL_NAME=$(python3 -c '
|
|
316
|
+
import sys, json
|
|
317
|
+
try:
|
|
318
|
+
with open(sys.argv[1]) as f:
|
|
319
|
+
b = json.load(f)
|
|
320
|
+
v = b.get("vertical")
|
|
321
|
+
print(v if isinstance(v, str) and v else "")
|
|
322
|
+
except Exception:
|
|
323
|
+
print("")
|
|
324
|
+
' "$BRAND_JSON_PATH")
|
|
325
|
+
if [ -n "$VERTICAL_NAME" ]; then
|
|
326
|
+
CANDIDATE="${PLATFORM_ROOT_RESOLVED}/plugins/memory/references/${VERTICAL_NAME}.md"
|
|
327
|
+
if [ -f "$CANDIDATE" ]; then
|
|
328
|
+
VERTICAL_PATH="$CANDIDATE"
|
|
329
|
+
fi
|
|
330
|
+
fi
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
# One Python pass:
|
|
334
|
+
# - reads the agent file and strips the frontmatter to recover the
|
|
335
|
+
# paragraph (the literal four sentences with `{schema}` and
|
|
336
|
+
# `{conversation}` placeholders);
|
|
337
|
+
# - reads schema-base.md (and concatenates the vertical when set);
|
|
338
|
+
# - renders the envelope's `turns` array into a flat transcript — one
|
|
339
|
+
# line per turn, `role: text`, with inline `[tool: name(args)]`
|
|
340
|
+
# markers where the assistant called tools (so the model sees the
|
|
341
|
+
# tool-shape it has been calling without having to read JSON);
|
|
342
|
+
# - substitutes both placeholders in the paragraph;
|
|
343
|
+
# - emits one tab-separated line of byte counts, then the
|
|
344
|
+
# spawn-body JSON containing the filled paragraph as `initialMessage`.
|
|
345
|
+
# The envelope rides through a tmpfile (not stdin) so the python script
|
|
346
|
+
# can be sourced from the heredoc; the two stdin redirections would
|
|
347
|
+
# otherwise collide and Python would see an empty envelope.
|
|
348
|
+
ENVELOPE_FILE=$(mktemp)
|
|
349
|
+
printf '%s' "$ENVELOPE" > "$ENVELOPE_FILE"
|
|
350
|
+
COMBINED=$(python3 - \
|
|
351
|
+
"$AGENT_BODY_PATH" "$SCHEMA_BASE_PATH" "$VERTICAL_PATH" "$ADMIN_SESSION_ID" "$ENVELOPE_FILE" <<'PY'
|
|
291
352
|
import sys, json
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
353
|
+
|
|
354
|
+
agent_path, schema_path, vertical_path, sid, envelope_path = sys.argv[1:6]
|
|
355
|
+
|
|
356
|
+
with open(agent_path, "r", encoding="utf-8") as f:
|
|
357
|
+
raw = f.read()
|
|
358
|
+
parts = raw.split("---", 2)
|
|
359
|
+
body_template = parts[2].lstrip("\n").rstrip() if len(parts) >= 3 else raw.rstrip()
|
|
360
|
+
|
|
361
|
+
with open(schema_path, "r", encoding="utf-8") as f:
|
|
362
|
+
schema_text = f.read()
|
|
363
|
+
if vertical_path:
|
|
364
|
+
with open(vertical_path, "r", encoding="utf-8") as f:
|
|
365
|
+
schema_text = schema_text.rstrip() + "\n\n" + f.read()
|
|
366
|
+
|
|
367
|
+
with open(envelope_path, "r", encoding="utf-8") as f:
|
|
368
|
+
envelope = json.load(f)
|
|
369
|
+
turns = envelope.get("turns") or []
|
|
370
|
+
lines = []
|
|
371
|
+
for t in turns:
|
|
372
|
+
role = t.get("role", "?")
|
|
373
|
+
text_t = t.get("text", "") or ""
|
|
374
|
+
marks = []
|
|
375
|
+
for tc in (t.get("toolCalls") or []):
|
|
376
|
+
tool = tc.get("tool", "?")
|
|
377
|
+
inp = tc.get("input")
|
|
378
|
+
try:
|
|
379
|
+
inp_s = json.dumps(inp, ensure_ascii=False) if inp is not None else ""
|
|
380
|
+
except Exception:
|
|
381
|
+
inp_s = str(inp)
|
|
382
|
+
marks.append(f"[tool: {tool}({inp_s})]")
|
|
383
|
+
if marks:
|
|
384
|
+
lines.append(f"{role}: {text_t} {' '.join(marks)}".rstrip())
|
|
385
|
+
else:
|
|
386
|
+
lines.append(f"{role}: {text_t}")
|
|
387
|
+
# Wrap the transcript block in newlines so each turn lands on its own
|
|
388
|
+
# line in the filled prompt — the template's `{conversation}` sits
|
|
389
|
+
# mid-sentence, so the leading newline separates the first transcript
|
|
390
|
+
# line from the surrounding prose, and the trailing newline separates
|
|
391
|
+
# the last one from the closing `, using the tools at your disposal.`
|
|
392
|
+
conversation_text = "\n" + "\n".join(lines) + "\n"
|
|
393
|
+
|
|
394
|
+
filled = body_template.replace("{schema}", schema_text).replace("{conversation}", conversation_text)
|
|
395
|
+
|
|
396
|
+
schema_bytes = len(schema_text.encode("utf-8"))
|
|
397
|
+
conv_bytes = len(conversation_text.encode("utf-8"))
|
|
398
|
+
body_bytes = len(filled.encode("utf-8"))
|
|
399
|
+
|
|
400
|
+
spawn_body = {
|
|
295
401
|
"adminSessionId": sid,
|
|
296
402
|
"channel": "browser",
|
|
297
403
|
"specialist": "database-operator",
|
|
298
404
|
"model": "haiku",
|
|
299
|
-
"initialMessage":
|
|
405
|
+
"initialMessage": filled,
|
|
300
406
|
}
|
|
301
|
-
print(json.dumps(body))
|
|
302
|
-
' "$ADMIN_SESSION_ID" "$ENVELOPE" 2>/dev/null)
|
|
303
407
|
|
|
304
|
-
|
|
305
|
-
|
|
408
|
+
print(f"{schema_bytes}\t{conv_bytes}\t{body_bytes}")
|
|
409
|
+
print(json.dumps(spawn_body))
|
|
410
|
+
PY
|
|
411
|
+
)
|
|
412
|
+
rm -f "$ENVELOPE_FILE"
|
|
413
|
+
|
|
414
|
+
BYTES_LINE=$(printf '%s' "$COMBINED" | head -n1)
|
|
415
|
+
SPAWN_BODY=$(printf '%s' "$COMBINED" | tail -n +2)
|
|
416
|
+
IFS=$'\t' read -r SCHEMA_BYTES CONV_BYTES BODY_BYTES <<<"$BYTES_LINE"
|
|
417
|
+
|
|
418
|
+
emit_log "substitution sessionId=${ADMIN_SESSION_ID} schemaBytes=${SCHEMA_BYTES} conversationBytes=${CONV_BYTES} bodyBytes=${BODY_BYTES}"
|
|
419
|
+
emit_log "spawn-request sessionId=${ADMIN_SESSION_ID} specialist=database-operator initialMessageBytes=${BODY_BYTES}"
|
|
306
420
|
|
|
307
421
|
SPAWN_RES_FILE=$(mktemp)
|
|
308
422
|
SPAWN_HTTP=$(curl -sS -o "$SPAWN_RES_FILE" -w '%{http_code}' -X POST \
|