@zihanw/pi-forge 0.1.0 → 0.2.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/src/compiler.ts CHANGED
@@ -8,6 +8,8 @@ import type {
8
8
  PromptStackItem,
9
9
  PromptStackRole,
10
10
  PromptStackSlotItem,
11
+ PromptStateScope,
12
+ PromptStateValue,
11
13
  PromptVariableStore,
12
14
  } from "./types.ts";
13
15
  import { SUPPORTED_SLOTS } from "./types.ts";
@@ -21,7 +23,7 @@ const ZERO_USAGE = {
21
23
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
22
24
  };
23
25
 
24
- export function createPromptVariableStore(sessionVariables: Record<string, string> = {}): PromptVariableStore {
26
+ export function createPromptVariableStore(sessionVariables: Record<string, PromptStateValue> = {}): PromptVariableStore {
25
27
  return { turn: {}, session: { ...sessionVariables }, sessionDirty: false };
26
28
  }
27
29
 
@@ -306,44 +308,163 @@ function renderVariables(
306
308
  runtime: PromptRuntime,
307
309
  ): string {
308
310
  const options = item.options ?? {};
309
- const includeStatic = options.includeStatic !== false;
310
- const includeSession = options.includeSession !== false;
311
- const includeTurn = options.includeTurn !== false;
311
+ const scopes = selectedVariableScopes(options);
312
+ const includeMetadata = options.includeMetadata === true;
313
+ const format = options.format === "json" ? "json" : "xml";
314
+ const includeNamespaces = normalizeStringArray(options.includeNamespaces);
315
+ const excludeNamespaces = normalizeStringArray(options.excludeNamespaces);
316
+ const maxValueChars = typeof options.maxValueChars === "number" && Number.isFinite(options.maxValueChars) && options.maxValueChars > 0
317
+ ? Math.floor(options.maxValueChars)
318
+ : undefined;
312
319
  const store = runtime.variables;
313
- const staticVars = stack.variables ?? {};
320
+ const grouped: Record<PromptStateScope, Record<string, PromptStateValue>> = {
321
+ static: {},
322
+ session: {},
323
+ turn: {},
324
+ };
314
325
 
315
- const parts: string[] = ["<prompt_variables>"];
326
+ if (scopes.has("static")) {
327
+ grouped.static = collectStaticVariables(stack);
328
+ }
329
+ if (scopes.has("session")) {
330
+ grouped.session = { ...collectDefaultVariables(stack, "session"), ...(store?.session ?? {}) };
331
+ }
332
+ if (scopes.has("turn")) {
333
+ grouped.turn = { ...collectDefaultVariables(stack, "turn"), ...(store?.turn ?? {}) };
334
+ }
316
335
 
317
- if (includeStatic && Object.keys(staticVars).length > 0) {
318
- parts.push(" <static>");
319
- for (const [key, value] of Object.entries(staticVars).sort(([a], [b]) => a.localeCompare(b))) {
320
- parts.push(` <${escapeXml(key)}>${escapeXml(value)}</${escapeXml(key)}>`);
321
- }
322
- parts.push(" </static>");
336
+ for (const scope of ["static", "session", "turn"] as const) {
337
+ grouped[scope] = Object.fromEntries(
338
+ Object.entries(grouped[scope]).filter(([name]) => shouldRenderVariable(name, includeNamespaces, excludeNamespaces)),
339
+ );
323
340
  }
324
341
 
325
- if (includeSession && store && Object.keys(store.session).length > 0) {
326
- parts.push(" <session>");
327
- for (const [key, value] of Object.entries(store.session).sort(([a], [b]) => a.localeCompare(b))) {
328
- parts.push(` <${escapeXml(key)}>${escapeXml(value)}</${escapeXml(key)}>`);
342
+ if (!Object.values(grouped).some((values) => Object.keys(values).length > 0)) return "";
343
+
344
+ if (format === "json") {
345
+ const payload: Record<string, unknown> = {};
346
+ for (const scope of ["static", "session", "turn"] as const) {
347
+ if (Object.keys(grouped[scope]).length === 0) continue;
348
+ payload[scope] = includeMetadata
349
+ ? Object.fromEntries(Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => [
350
+ name,
351
+ {
352
+ value,
353
+ ...metadataForVariable(stack, name),
354
+ },
355
+ ]))
356
+ : Object.fromEntries(Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b)));
329
357
  }
330
- parts.push(" </session>");
358
+ return `<prompt_state format=\"json\">\n${escapeXml(JSON.stringify(payload, null, 2))}\n</prompt_state>`;
331
359
  }
332
360
 
333
- if (includeTurn && store && Object.keys(store.turn).length > 0) {
334
- parts.push(" <turn>");
335
- for (const [key, value] of Object.entries(store.turn).sort(([a], [b]) => a.localeCompare(b))) {
336
- parts.push(` <${escapeXml(key)}>${escapeXml(value)}</${escapeXml(key)}>`);
361
+ const parts: string[] = ["<prompt_state>"];
362
+
363
+ for (const scope of ["static", "session", "turn"] as const) {
364
+ const entries = Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b));
365
+ if (entries.length === 0) continue;
366
+ parts.push(` <${scope}>`);
367
+ for (const [name, value] of entries) {
368
+ const metadata = includeMetadata ? metadataForVariable(stack, name) : {};
369
+ const attrs = [
370
+ `name=\"${escapeXml(name)}\"`,
371
+ `type=\"${escapeXml(metadata.type ?? inferStateValueType(value))}\"`,
372
+ ];
373
+ if (metadata.description) attrs.push(`description=\"${escapeXml(metadata.description)}\"`);
374
+ if (metadata.agentWritable !== undefined) attrs.push(`agentWritable=\"${metadata.agentWritable ? "true" : "false"}\"`);
375
+ if (metadata.userWritable !== undefined) attrs.push(`userWritable=\"${metadata.userWritable ? "true" : "false"}\"`);
376
+ parts.push(` <var ${attrs.join(" ")}>${escapeXml(truncateValue(stateValueToPromptText(value), maxValueChars))}</var>`);
337
377
  }
338
- parts.push(" </turn>");
378
+ parts.push(` </${scope}>`);
339
379
  }
340
380
 
341
- parts.push("</prompt_variables>");
381
+ parts.push("</prompt_state>");
382
+ return parts.join("\n");
383
+ }
342
384
 
343
- // Don't emit empty variables block
344
- if (parts.length <= 2) return "";
385
+ function selectedVariableScopes(options: Record<string, unknown>): Set<PromptStateScope> {
386
+ const explicit = normalizeStringArray(options.includeScopes).filter(isStateScope);
387
+ if (explicit.length > 0) return new Set(explicit);
345
388
 
346
- return parts.join("\n");
389
+ const scopes = new Set<PromptStateScope>();
390
+ if (options.includeStatic !== false) scopes.add("static");
391
+ if (options.includeSession !== false) scopes.add("session");
392
+ if (options.includeTurn !== false) scopes.add("turn");
393
+ return scopes;
394
+ }
395
+
396
+ function collectStaticVariables(stack: PromptStack): Record<string, PromptStateValue> {
397
+ return {
398
+ ...collectDefaultVariables(stack, "static"),
399
+ ...(stack.variables ?? {}),
400
+ };
401
+ }
402
+
403
+ function collectDefaultVariables(stack: PromptStack, scope: PromptStateScope): Record<string, PromptStateValue> {
404
+ const values: Record<string, PromptStateValue> = {};
405
+ for (const [name, definition] of Object.entries(stack.state?.definitions ?? {})) {
406
+ if ((definition.scope ?? "session") !== scope) continue;
407
+ if (definition.default !== undefined) values[name] = definition.default;
408
+ }
409
+ return values;
410
+ }
411
+
412
+ function shouldRenderVariable(name: string, includePatterns: string[], excludePatterns: string[]): boolean {
413
+ if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesNamespace(name, pattern))) return false;
414
+ if (excludePatterns.some((pattern) => matchesNamespace(name, pattern))) return false;
415
+ return true;
416
+ }
417
+
418
+ function matchesNamespace(name: string, pattern: string): boolean {
419
+ if (!pattern) return false;
420
+ if (pattern.endsWith("*")) return name.startsWith(pattern.slice(0, -1));
421
+ return name === pattern;
422
+ }
423
+
424
+ function metadataForVariable(stack: PromptStack, name: string): {
425
+ type?: string;
426
+ description?: string;
427
+ agentWritable?: boolean;
428
+ userWritable?: boolean;
429
+ } {
430
+ const definition = stack.state?.definitions?.[name];
431
+ if (!definition) return {};
432
+ return {
433
+ type: definition.type,
434
+ description: definition.description,
435
+ agentWritable: definition.agentWritable,
436
+ userWritable: definition.userWritable,
437
+ };
438
+ }
439
+
440
+ function normalizeStringArray(value: unknown): string[] {
441
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
442
+ }
443
+
444
+ function isStateScope(value: string): value is PromptStateScope {
445
+ return value === "static" || value === "session" || value === "turn";
446
+ }
447
+
448
+ function inferStateValueType(value: PromptStateValue): string {
449
+ if (value === null) return "null";
450
+ if (Array.isArray(value)) return "array";
451
+ return typeof value;
452
+ }
453
+
454
+ function truncateValue(value: string, maxChars: number | undefined): string {
455
+ if (!maxChars || value.length <= maxChars) return value;
456
+ return `${value.slice(0, maxChars)}\n[truncated]`;
457
+ }
458
+
459
+ function stateValueToPromptText(value: PromptStateValue): string {
460
+ if (typeof value === "string") return value;
461
+ return JSON.stringify(value, null, 2);
462
+ }
463
+
464
+ function stateValueToMacroText(value: PromptStateValue | undefined): string {
465
+ if (value === undefined) return "";
466
+ if (typeof value === "string") return value;
467
+ return JSON.stringify(value);
347
468
  }
348
469
 
349
470
  function expandMacros(
@@ -354,7 +475,6 @@ function expandMacros(
354
475
  itemId: string,
355
476
  ): string {
356
477
  const policy = stack.defaults?.unresolvedMacroPolicy ?? "warn";
357
- const staticVariables = stack.variables ?? {};
358
478
  const unknown = new Set<string>();
359
479
 
360
480
  const result = text.replace(/\{\{([^{}]+)\}\}/g, (full, rawName: string) => {
@@ -386,9 +506,9 @@ function expandMacros(
386
506
  unknown.add(expression);
387
507
  return full;
388
508
  }
389
- if (command === "getturnvar") return runtime.variables?.turn[name] ?? "";
390
- if (command === "getsessionvar") return runtime.variables?.session[name] ?? "";
391
- return getRuntimeVariable(runtime, staticVariables, name) ?? "";
509
+ if (command === "getturnvar") return stateValueToMacroText(runtime.variables?.turn[name]);
510
+ if (command === "getsessionvar") return stateValueToMacroText(runtime.variables?.session[name]);
511
+ return stateValueToMacroText(getRuntimeVariable(runtime, stack, name));
392
512
  }
393
513
 
394
514
  if (command === "clearvar" || command === "clearturnvar" || command === "clearsessionvar") {
@@ -406,8 +526,8 @@ function expandMacros(
406
526
  const dynamicValue = getBuiltinMacro(command, runtime);
407
527
  if (dynamicValue !== undefined) return dynamicValue;
408
528
 
409
- const variableValue = getRuntimeVariable(runtime, staticVariables, command);
410
- if (variableValue !== undefined) return variableValue;
529
+ const variableValue = getRuntimeVariable(runtime, stack, command);
530
+ if (variableValue !== undefined) return stateValueToMacroText(variableValue);
411
531
 
412
532
  unknown.add(expression);
413
533
  return full;
@@ -431,6 +551,8 @@ function getBuiltinMacro(name: string, runtime: PromptRuntime): string | undefin
431
551
  return runtime.options.cwd;
432
552
  case "date":
433
553
  return formatDate(runtime.now);
554
+ case "time":
555
+ return formatTime(runtime.now);
434
556
  case "lastUserMessage":
435
557
  return runtime.latestUserMessage ?? "";
436
558
  case "selectedTools":
@@ -445,9 +567,10 @@ function getBuiltinMacro(name: string, runtime: PromptRuntime): string | undefin
445
567
  }
446
568
  }
447
569
 
448
- function getRuntimeVariable(runtime: PromptRuntime, staticVariables: Record<string, string>, name: string): string | undefined {
570
+ function getRuntimeVariable(runtime: PromptRuntime, stack: PromptStack, name: string): PromptStateValue | undefined {
449
571
  if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.turn, name)) return runtime.variables.turn[name];
450
572
  if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) return runtime.variables.session[name];
573
+ const staticVariables = collectStaticVariables(stack);
451
574
  if (Object.prototype.hasOwnProperty.call(staticVariables, name)) return staticVariables[name];
452
575
  return undefined;
453
576
  }
@@ -537,6 +660,13 @@ function formatDate(now: Date): string {
537
660
  return `${year}-${month}-${day}`;
538
661
  }
539
662
 
663
+ function formatTime(now: Date): string {
664
+ const hours = String(now.getHours()).padStart(2, "0");
665
+ const minutes = String(now.getMinutes()).padStart(2, "0");
666
+ const seconds = String(now.getSeconds()).padStart(2, "0");
667
+ return `${hours}:${minutes}:${seconds}`;
668
+ }
669
+
540
670
  function escapeXml(value: string): string {
541
671
  return value
542
672
  .replace(/&/g, "&amp;")