Skip to content

L0: model-written text in text slots, strict actions, in-card chat - #53

Merged
ymote merged 1 commit into
mainfrom
feat/l0-ai-text-and-chat
Oct 2, 2026
Merged

ymote merged 1 commit into
mainfrom
feat/l0-ai-text-and-chat

Conversation

@ymote

@ymote ymote commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Why

Design decision (2026-10-01): L0 cards are no longer only real-time generated layouts. The model fills them with its own words (summaries, drafts, research reports), and people chat with the app's agent inside a card. Until now the checker refused model-written text anywhere on screen: in check_path for rendering, and in check_event_batch for set(copy.x). On this main those checks were at lib.rs:5720 and :4447, not at the 5619/4412 the brief cited.

The new rules (profile §4.2, §5.15)

What counts as model text: a model-copy declaration, a model-written host field (catalog::MODEL_TEXT: sys.digest summary/text/label, sys.chat text), or a text state that such text was written into (a draft).

Words: allowed in a text slot, marked AI-written.

  • Text slots (catalog::TEXT_SLOTS) are the text argument of TextHero, TextTitle, TextBody, TextRow, TextEyebrow, TextCaption, Band, Bubble, ChatEntry and Field. They do not include chip, tile and tab labels, avatars, glyphs, suffixes, Kit args or placeholders.
  • Model text carries ValueOrigin::Model, which survives combine. ai_written(&UiNode) decides the mark, and all three lowerings emit it:
    • kit::lower wraps the call in l0_ai_text(…), innermost (Field included).
    • makepad::lower adds l0_ai: true.
    • lower_dsl adds ai: 1.
  • Plain text only. It is emitted as a quoted literal and never becomes markup, a target or code.
  • A transition may set(path) model text into a text state (a draft). That state then counts as model text, including through chained sets. Dispatch now resolves set(copy.x) from the card's own copy, which it previously looked up in the data blob, where it never is.

Actions: strict, default deny. check_path refuses model text in every position except a catalogued text slot. That covers payloads (value:), guards, loop keys, source arguments (including state.<draft>), control labels, component props, Kit args, state initials, writes into non-text states, and writes into host stores (sys.link, sys.watchlist, …).

P1 fix: makepad::lower built a Field's on_return target as head + t + tail around a "$$" hole without escaping t. Typing ","e":"drop","v":" therefore redirected the event, because a JSON parser keeps the last e. Both on_return and (newly lowered) on_change now emit a serialized routing head plus sys.json_string(t). This is the construction kit::tap_target already uses.

Chat:

  • source convo sys.chat(app, thread, fields) is host-answered only (no vm_call). It is scoped like sys.digest, with a literal app and a thread id or path, and answers entries {id, role, text, at} plus status and count. role is one of user, model or host.
  • It accepts one write, append. The payload is what the user committed in a Field. The host appends the agent's reply, and a card can never write a model entry.
  • ChatEntry(text: m.text, role: m.role) draws a bubble on the side its role says. Both arguments must read the same sys.chat row, so a card cannot present model text as the user's. Only model entries get the AI-written mark.

Tests

  • New tests/ai_text.rs (12 tests) and fixtures/chat.card. They cover:
    • marking in all three lowerings
    • digest fields followed through a loop
    • a draft: dispatch, origin, marking, and the mark dropping after the user commits
    • 14 forbidden positions plus a source argument, a host store, an enum write and an initial
    • laundering through a draft
    • injection-shaped strings (l0:{…}, " + sys.shell(…) + ", javascript: links, NUL, set: verbs) staying inert literals
    • typed text unable to redirect a Field target, simulated against the emitted head
    • the chat transcript, send, and misuse (scoping, append-only, mismatched role and text)
  • Two profile tests encoded the old "model copy is never rendered" rule. They are rewritten for the new rule and still assert the refusals.
  • cargo test -p octoscript-ui-l0: 310 passed, 0 failed. cargo clippy -p octoscript-ui-l0 --all-targets -- -D warnings: clean. cargo check -p octoscript-core --all-targets: clean.

Downstream changes needed (not in this PR)

Octoscript-Makepad (kit and renderer)

  • _kit.octoscript needs fn l0_ai_text(node) { node.ai = 1 return node }. Until it exists, a card containing model text will not evaluate.
  • octoscript-node Attrs and octoscript-render eval.rs need to read ai, and the renderer needs to draw a visible AI-written treatment.
  • The makepad path needs to accept or ignore l0_ai: true the way it does l0_event:.
  • AI-written text must render as a plain label: no markdown or HTML, no autolinking.
  • ChatEntry reuses the existing l0_bubble_me, l0_bubble_them and *_long functions, so it needs no new kit role.

OctoSense (host)

  • Resolve sys.chat for the publishing app only:
    • check app against the publisher;
    • store threads;
    • on an append CollectionWrite, record a user entry, run the app's agent, and append its reply as a model entry (or a host notice);
    • mark the source stale.
  • Enforce length and rate limits and retention.
  • Never let a card write a model entry.
  • Dispatch Field events with event_payload_origin/dispatch_reporting_with_origin, which field_target_expr already does structurally.
  • Regenerate apps/appcard/a2app-l0/framework/catalog.md with gen-catalog.py. Running it locally adds ChatEntry and sys.chat, and also the earlier sys.digest, sys.dataset and sys.news_* entries that the current copy is missing.
  • Update framework/l0.md (line 140 says model-copy "may not be rendered") and docs/LEDGER-ARCHITECTURE.md to the new rule.
  • Do not ship model-text cards until the kit's l0_ai_text lands.

🤖 Generated with Claude Code

Cards now carry the model's own words (summaries, drafts, reports) and an
in-card chat with the app's agent. Profile §4.2 and §5.15.

Model text (model-copy, model-written host fields such as sys.digest's
summary/text/label and sys.chat's text, and any text state such text is
written into) is:
- allowed in a catalogued text slot (TextHero/Title/Body/Row/Eyebrow/
  Caption, Band, Bubble, ChatEntry, Field .text), carrying
  ValueOrigin::Model and marked AI-written by every lowering:
  kit `l0_ai_text(...)`, makepad ` l0_ai: true`, DSL `ai: 1`, and
  `ai_written(&UiNode)`;
- allowed in `set(path)` into a `text` state (a draft), which is then model
  text to a fixed point; dispatch now resolves `set(copy.x)` from the card;
- refused everywhere else, default deny: payloads, guards, loop keys, source
  arguments, control labels, component props, Kit args, state initials,
  non-text states and host stores.

P1 fix: makepad::lower built Field targets by splicing the typed text into
a JSON string unescaped, so typed text could redirect the event. on_return
(and now on_change) emit a serialized routing head plus sys.json_string(t).

Chat: `sys.chat(app, thread, fields)` (host-answered, scoped like
sys.digest, append-only) answering entries {id, role, text, at}, and the
`ChatEntry(text: m.text, role: m.role)` role, which must read text and role
off the same sys.chat row; `model` entries are marked AI-written.

Docs: ui-profile-l0.md §4 table, §4.2, §5.14 note, §5.15; constructor TOML.
Tests: tests/ai_text.rs and fixtures/chat.card; two profile tests updated
to the new rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote merged commit 5991dfa into main Oct 2, 2026
8 checks passed
ymote added a commit to OctoSense-org/OctoSense that referenced this pull request Oct 2, 2026
…0c (L0 AI text and chat merged)

The two changes this pull request builds against are on their mains:
OctoSense-org/OctoScript#53 (5991dfae, model-written text in text slots,
strict actions, in-card chat) and OctoSense-org/OctoScript-Makepad#68
(8f103d0c, the l0_ai_text kit stamp and the AI-written mark; its
runtime.json pins Octoscript 5991dfae and Makepad e29a0eaa, unchanged).
native-runtime.lock.json (here and AppCard's) and the root manifest's
Octoscript and Octoscript-Makepad revs move to them; the crates resolve
from .sources through [patch], so Cargo.lock does not change.

Checked: python3 tools/setup.py --update, then --check --cargo; python3
tools/native_apps.py --check; cargo check -p octosense; cargo test -p
octosense-shell --features app-hub --lib (715 passed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant