refactor: renamed intent field from _i to i
- Renamed the global `INTENT_FIELD` constant from `_i` to `i`. - Updated documentation strings, type annotations, and test expectations across packages to reflect the new field name. - Ensured consistent usage of the constant in tool schema construction and intent serialization.
This commit is contained in:
@@ -591,7 +591,7 @@ export function normalizeTools(
|
||||
// specs without their descriptions (top-level + nested schema annotations)
|
||||
// so they are not duplicated on the wire. Strip the STABLE wire schema (the
|
||||
// memoized `stripSchemaDescriptions` result is reused across requests), then
|
||||
// re-inject `_i` (without its hint, which `describeIntent: false` omits) so
|
||||
// re-inject `i` (without its hint, which `describeIntent: false` omits) so
|
||||
// intent tracing keeps the field while no descriptions ride the wire.
|
||||
if (pruneDescriptions) {
|
||||
let parameters = stripSchemaDescriptions(toolWireSchema(t)) as TSchema;
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface StablePrefixSnapshot {
|
||||
|
||||
/** Options threaded through `build()` so the snapshot reflects loop-time settings. */
|
||||
export interface BuildOptions {
|
||||
/** Inject the `_i` intent field into tool schemas (must match agent-loop's normalizeTools). */
|
||||
/** Inject the `i` intent field into tool schemas (must match agent-loop's normalizeTools). */
|
||||
intentTracing: boolean;
|
||||
exampleDialect?: Dialect;
|
||||
/** Strip tool descriptions from the provider-bound specs (must match normalizeTools). */
|
||||
|
||||
@@ -273,7 +273,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
||||
*
|
||||
* When set, the loop reads messages from the append-only log (stable
|
||||
* byte prefix) and caches system prompt + tools. Tools exclude per-turn
|
||||
* `_i` intent fields.
|
||||
* `i` intent fields.
|
||||
*/
|
||||
appendOnlyContext?: AppendOnlyContextManager;
|
||||
|
||||
@@ -584,11 +584,11 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
|
||||
*/
|
||||
interruptible?: boolean;
|
||||
/**
|
||||
* Controls how the INTENT_FIELD (`_i`) is handled for this tool.
|
||||
* - `"require"` (default): `_i` is injected and required in the parameter schema.
|
||||
* - `"optional"`: `_i` is injected as an optional/nullable field.
|
||||
* - `"omit"`: `_i` is NOT injected. Use for tools where intent is obvious (yield, resolve, todo, …).
|
||||
* - function: `_i` is NOT injected; intent is derived dynamically from (potentially partial / streaming) args.
|
||||
* Controls how the INTENT_FIELD (`i`) is handled for this tool.
|
||||
* - `"require"` (default): `i` is injected and required in the parameter schema.
|
||||
* - `"optional"`: `i` is injected as an optional/nullable field.
|
||||
* - `"omit"`: `i` is NOT injected. Use for tools where intent is obvious (yield, resolve, todo, …).
|
||||
* - function: `i` is NOT injected; intent is derived dynamically from (potentially partial / streaming) args.
|
||||
*/
|
||||
intent?: "omit" | "optional" | "require" | ((args: Partial<Static<TParameters>>) => string | undefined);
|
||||
|
||||
|
||||
@@ -592,7 +592,7 @@ describe("message sync", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("intent injection through build()", () => {
|
||||
it("injects required `_i` into tool schemas when intentTracing is true", () => {
|
||||
it("injects required `i` into tool schemas when intentTracing is true", () => {
|
||||
const mgr = new AppendOnlyContextManager();
|
||||
const tool = makeTool("read", "Read", {
|
||||
type: "object",
|
||||
@@ -608,20 +608,20 @@ describe("intent injection through build()", () => {
|
||||
expect(params!.required).toContain(INTENT_FIELD);
|
||||
});
|
||||
|
||||
it("materializes ArkType params and keeps `_i` first in authored order", () => {
|
||||
it("materializes ArkType params and keeps `i` first in authored order", () => {
|
||||
const mgr = new AppendOnlyContextManager();
|
||||
const tool = makeTool("write", "Write", type({ path: "string", content: "string" }));
|
||||
const ctx = makeContext({ tools: [tool] });
|
||||
|
||||
const result = mgr.build(ctx, { intentTracing: true });
|
||||
const params = result.tools?.[0]?.parameters as { properties?: Record<string, unknown>; required?: string[] };
|
||||
// `_i` must lead; authored order (path before content) is preserved rather
|
||||
// `i` must lead; authored order (path before content) is preserved rather
|
||||
// than ArkType's alphabetized-by-hash order (content, path).
|
||||
expect(Object.keys(params.properties ?? {})).toEqual([INTENT_FIELD, "path", "content"]);
|
||||
expect(params.required).toContain(INTENT_FIELD);
|
||||
});
|
||||
|
||||
it("omits `_i` when intentTracing is false", () => {
|
||||
it("omits `i` when intentTracing is false", () => {
|
||||
const mgr = new AppendOnlyContextManager();
|
||||
const tool = makeTool("read", "Read", {
|
||||
type: "object",
|
||||
@@ -679,7 +679,7 @@ describe("tool examples injection through build()", () => {
|
||||
expect(desc).toBe("Find files.");
|
||||
});
|
||||
|
||||
it("injects the `_i` placeholder into examples when intentTracing is on", () => {
|
||||
it("injects the `i` placeholder into examples when intentTracing is on", () => {
|
||||
const mgr = new AppendOnlyContextManager();
|
||||
const tool = makeTool("find", "Find files.", findParams, findExamples);
|
||||
const ctx = makeContext({ tools: [tool] });
|
||||
|
||||
@@ -9,7 +9,7 @@ export function renderToolExamples(tool: InbandTool, dialect: Dialect, intentFie
|
||||
if (!examples?.length) return "";
|
||||
const definition = getDialectDefinition(dialect);
|
||||
const renderCall = (args: Record<string, unknown>): string => {
|
||||
// When intent tracing injects `_i` into the schema, examples must show a
|
||||
// When intent tracing injects `i` into the schema, examples must show a
|
||||
// placeholder so the model learns to emit it. Keep it first, matching the
|
||||
// schema injection order.
|
||||
const finalArgs = intentField ? { [intentField]: INTENT_PLACEHOLDER, ...args } : args;
|
||||
|
||||
@@ -23,7 +23,7 @@ Call with a verbatim body — everything between `«` and `»` is taken literall
|
||||
|
||||
Argument values:
|
||||
|
||||
- Strings are written bare and verbatim (`path=src/a.ts`). Quote with `"…"` only when the value contains spaces or starts with `"`, `[`, or `{` (`_i="run the tests"`).
|
||||
- Strings are written bare and verbatim (`path=src/a.ts`). Quote with `"…"` only when the value contains spaces or starts with `"`, `[`, or `{` (`i="run the tests"`).
|
||||
- Numbers, booleans, and `null` are JSON literals (`offset=50`, `force=true`).
|
||||
- Arrays and objects are inline JSON (`paths=["src","test"]`).
|
||||
- The body fence holds the call's first long/multi-line string parameter; its key is implied, never written.
|
||||
|
||||
@@ -656,8 +656,8 @@ export interface Tool<TParameters extends TSchema = TSchema> {
|
||||
* Illustrative calls/notes; the AI layer renders them into an `<examples>`
|
||||
* block in the model's native tool-call syntax and appends to the wire
|
||||
* description. Author `call`/`bad`/`good` as plain argument objects WITHOUT
|
||||
* `_i` — when intent tracing injects `_i` into the schema, the renderer adds
|
||||
* a placeholder `_i` automatically. Type each tool's `examples` against its
|
||||
* `i` — when intent tracing injects `i` into the schema, the renderer adds
|
||||
* a placeholder `i` automatically. Type each tool's `examples` against its
|
||||
* own schema (e.g. `readonly ToolExample<typeof schema["type"]>[]`).
|
||||
*/
|
||||
examples?: readonly ToolExample[];
|
||||
|
||||
@@ -65,8 +65,8 @@ describe("processResponsesStream: parallel function_call items", () => {
|
||||
const emitted: EmittedEvent[] = [];
|
||||
const stream = { push: (e: unknown) => emitted.push(e as EmittedEvent), end: () => {} } as never;
|
||||
|
||||
const argsA = JSON.stringify({ _i: "Reading test", path: "test.txt" });
|
||||
const argsB = JSON.stringify({ _i: "Reading test", path: "test.md" });
|
||||
const argsA = JSON.stringify({ i: "Reading test", path: "test.txt" });
|
||||
const argsB = JSON.stringify({ i: "Reading test", path: "test.md" });
|
||||
|
||||
await processResponsesStream(
|
||||
makeStream([
|
||||
@@ -125,8 +125,8 @@ describe("processResponsesStream: parallel function_call items", () => {
|
||||
expect(blockA?.type).toBe("toolCall");
|
||||
expect(blockB?.type).toBe("toolCall");
|
||||
if (blockA?.type !== "toolCall" || blockB?.type !== "toolCall") throw new Error("expected toolCalls");
|
||||
expect(blockA.arguments).toEqual({ _i: "Reading test", path: "test.txt" });
|
||||
expect(blockB.arguments).toEqual({ _i: "Reading test", path: "test.md" });
|
||||
expect(blockA.arguments).toEqual({ i: "Reading test", path: "test.txt" });
|
||||
expect(blockB.arguments).toEqual({ i: "Reading test", path: "test.md" });
|
||||
|
||||
const ends = emitted.filter(e => e.type === "toolcall_end") as Array<{
|
||||
toolCall: { id: string; arguments: Record<string, unknown> };
|
||||
@@ -134,8 +134,8 @@ describe("processResponsesStream: parallel function_call items", () => {
|
||||
}>;
|
||||
expect(ends).toHaveLength(2);
|
||||
const byCallId = new Map(ends.map(e => [e.toolCall.id.split("|")[0], e]));
|
||||
expect(byCallId.get("call_a")?.toolCall.arguments).toEqual({ _i: "Reading test", path: "test.txt" });
|
||||
expect(byCallId.get("call_b")?.toolCall.arguments).toEqual({ _i: "Reading test", path: "test.md" });
|
||||
expect(byCallId.get("call_a")?.toolCall.arguments).toEqual({ i: "Reading test", path: "test.txt" });
|
||||
expect(byCallId.get("call_b")?.toolCall.arguments).toEqual({ i: "Reading test", path: "test.md" });
|
||||
expect(byCallId.get("call_a")?.contentIndex).toBe(0);
|
||||
expect(byCallId.get("call_b")?.contentIndex).toBe(1);
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ function chunk(model: string, delta: SseChoiceDelta, finish: SseChunk["choices"]
|
||||
const REPORTED_DSML_LEAK =
|
||||
"<|DSML|tool_calls>\n" +
|
||||
' <|DSML|invoke name="bash">\n' +
|
||||
' <|DSML|parameter name="_i" string="true">Check Fedora 42 available packages</|DSML|parameter>\n' +
|
||||
' <|DSML|parameter name="i" string="true">Check Fedora 42 available packages</|DSML|parameter>\n' +
|
||||
' <|DSML|parameter name="command" string="true">docker run --rm --platform linux/arm64 fedora:42 bash -c \'type python3; type git; type sed; type cp; ls /usr/bin/python3 2>/dev/null; rpm -qa | grep -E "^python3|^git-|^sed-|^bash-" | sort\'</|DSML|parameter>\n' +
|
||||
' <|DSML|parameter name="timeout" string="false">15</|DSML|parameter>\n' +
|
||||
" </|DSML|invoke>\n" +
|
||||
@@ -616,7 +616,7 @@ describe("Ollama provider DSML envelope healing", () => {
|
||||
expect(toolCalls).toHaveLength(1);
|
||||
expect(toolCalls[0].name).toBe("bash");
|
||||
expect(toolCalls[0].arguments).toMatchObject({
|
||||
_i: "Check Fedora 42 available packages",
|
||||
[INTENT_FIELD]: "Check Fedora 42 available packages",
|
||||
timeout: 15,
|
||||
});
|
||||
expect(String(toolCalls[0].arguments.command)).toContain("docker run");
|
||||
|
||||
@@ -186,6 +186,6 @@ describe("renderToolExamples", () => {
|
||||
examples: [{ caption: "Find files", call: { paths: ["src/**/*.ts"] } }],
|
||||
};
|
||||
|
||||
expect(renderToolExamples(tool, "anthropic")).not.toContain(INTENT_FIELD);
|
||||
expect(renderToolExamples(tool, "anthropic")).not.toContain(`<parameter name="${INTENT_FIELD}"`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ if "__omp_prelude_loaded__" not in globals():
|
||||
from pathlib import Path
|
||||
import os, json, math, re
|
||||
from urllib.parse import unquote
|
||||
INTENT_FIELD = "_i"
|
||||
INTENT_FIELD = "i"
|
||||
|
||||
# __omp_display is injected by runner.py before the prelude executes; it
|
||||
# mirrors IPython's display() semantics with the same MIME bundle output.
|
||||
|
||||
@@ -186,7 +186,7 @@ export class EventController {
|
||||
}
|
||||
#updateWorkingMessageFromIntent(intent: unknown): void {
|
||||
if (this.ctx.session.isAborting) return;
|
||||
// Streamed JSON can deliver non-string `_i` (object, number, boolean) before
|
||||
// Streamed JSON can deliver non-string `i` (object, number, boolean) before
|
||||
// schema validation; `?.` only guards null/undefined, so guard the type too.
|
||||
if (typeof intent !== "string") return;
|
||||
const trimmed = intent.trim();
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("Python tool bridge HTTP server", () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(body).toEqual({ ok: true, value: "file body" });
|
||||
expect(calls).toHaveLength(1);
|
||||
// `_i` survives the bridge round trip so transcript renderers have a label.
|
||||
// `i` survives the bridge round trip so transcript renderers have a label.
|
||||
expect((calls[0]!.args as Record<string, unknown>)[INTENT_FIELD]).toBe("py prelude");
|
||||
} finally {
|
||||
unregister();
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("formatSessionDumpText markdown-headings transcript", () => {
|
||||
expect(out).toContain("### Tool Result: read");
|
||||
expect(out).toContain("### Tool Call: read");
|
||||
expect(out).toContain("path: src/foo.ts");
|
||||
// The `_i` intent renders as a `//` comment under the heading, never inside the YAML args.
|
||||
// The `i` intent renders as a `//` comment under the heading, never inside the YAML args.
|
||||
expect(out).toContain("// Reading the file");
|
||||
expect(out).not.toContain(`${INTENT_FIELD}:`);
|
||||
// Tool calls render as a readable heading + YAML, never the <invoke>/<parameter> XML.
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface ToolViewProps {
|
||||
result?: ToolResultLike;
|
||||
/** Tool is still executing (live collab view). */
|
||||
running?: boolean;
|
||||
/** Model-provided intent (`_i`), shown atop the body. */
|
||||
/** Model-provided intent (`i`), shown atop the body. */
|
||||
intent?: string;
|
||||
/** Streaming partial output tail while running. */
|
||||
partial?: string;
|
||||
|
||||
@@ -49,7 +49,7 @@ export interface ToolRenderHost {
|
||||
export interface ToolRenderProps {
|
||||
/** Wire tool name (may be an alias of the registry key, e.g. `grep` → search). */
|
||||
name: string;
|
||||
/** Parsed tool-call arguments with the internal `_i` intent already stripped. */
|
||||
/** Parsed tool-call arguments with the internal `i` intent already stripped. */
|
||||
args: Record<string, unknown>;
|
||||
result?: ToolResultLike;
|
||||
/** Tool is still executing (live collab view). */
|
||||
|
||||
@@ -772,8 +772,8 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
|
||||
if (uselessCallIds.has(block.id)) continue;
|
||||
flushAssistant();
|
||||
const args = block.arguments as Record<string, unknown>;
|
||||
// Prefer the harness-derived intent, else the raw `_i` arg; render it as
|
||||
// a one-line `//comment` and drop `_i` from the args below.
|
||||
// Prefer the harness-derived intent, else the raw intent arg; render it as
|
||||
// a one-line `//comment` and drop it from the args below.
|
||||
const rawIntent =
|
||||
typeof block.intent === "string"
|
||||
? block.intent
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import type { AssistantMessage, Message, Usage } from "@oh-my-pi/pi-ai";
|
||||
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
||||
import * as snapcompact from "../src";
|
||||
|
||||
// Small frames keep render time negligible. Legacy 5x8 shape: 320px → 64 cols
|
||||
@@ -567,11 +568,16 @@ describe("serializeConversation", () => {
|
||||
expect(out).toBe("# User ¶\ndo the thing\n\n# Assistant ¶\ndone");
|
||||
});
|
||||
|
||||
it("merges a tool call with its paired result into one block, _i as a // comment", () => {
|
||||
it("merges a tool call with its paired result into one block, intent as a // comment", () => {
|
||||
const out = snapcompact.serializeConversation(
|
||||
[
|
||||
createAssistantMessage([
|
||||
{ type: "toolCall", id: "c1", name: "bash", arguments: { _i: "Running tests", command: "bun test" } },
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "c1",
|
||||
name: "bash",
|
||||
arguments: { [INTENT_FIELD]: "Running tests", command: "bun test" },
|
||||
},
|
||||
]),
|
||||
{ ...createToolResultMessage("3 pass"), toolCallId: "c1" } as Message,
|
||||
],
|
||||
@@ -580,21 +586,21 @@ describe("serializeConversation", () => {
|
||||
expect(out).toBe('# Tool call ¶\n//Running tests\nbash(command="bun test")\n<out>\n3 pass\n</out>');
|
||||
});
|
||||
|
||||
it("prefers the harness-derived intent over the raw _i arg and squashes newlines", () => {
|
||||
it("prefers the harness-derived intent over the raw intent arg and squashes newlines", () => {
|
||||
const out = snapcompact.serializeConversation([
|
||||
createAssistantMessage([
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "c1",
|
||||
name: "bash",
|
||||
arguments: { _i: "raw arg", command: "ls" },
|
||||
arguments: { [INTENT_FIELD]: "raw arg", command: "ls" },
|
||||
intent: "Derived\nintent line",
|
||||
},
|
||||
]),
|
||||
]);
|
||||
expect(out).toContain("//Derived intent line");
|
||||
expect(out).not.toContain("raw arg");
|
||||
expect(out).not.toContain("_i=");
|
||||
expect(out).not.toContain(`${INTENT_FIELD}=`);
|
||||
});
|
||||
|
||||
it("folds thinking into the assistant block as italics above the text", () => {
|
||||
|
||||
@@ -346,7 +346,7 @@ export type WireFrame = GuestFrame | HostFrame;
|
||||
export const COLLAB_PROTO = 1;
|
||||
|
||||
/** Parameter key used for intent tracing (e.g. prompt explanation/reasoning) */
|
||||
export const INTENT_FIELD = "_i";
|
||||
export const INTENT_FIELD = "i";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Envelope & link constants
|
||||
|
||||
Reference in New Issue
Block a user