Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0cdbbd0
Fix WebSocket event type imports in agent-server-client
steveluc Jan 26, 2026
b808df4
Add grammar generation infrastructure and NFA cache integration
steveluc Jan 29, 2026
e722795
Merge remote-tracking branch 'origin/main' into add-grammar-generatio…
steveluc Jan 29, 2026
bb2d6a4
Update pnpm-lock.yaml after merge
steveluc Jan 29, 2026
8278a3e
Add copyright headers and run prettier
steveluc Jan 29, 2026
b80d9b1
Exclude grammar generator integration tests from CI
steveluc Jan 29, 2026
2e5a844
Fix trademark section format in TEST_README.md
steveluc Jan 29, 2026
3bd2790
Sort package.json scripts alphabetically
steveluc Jan 29, 2026
badbc14
Remove unnecessary error handling in appAgentManager
steveluc Jan 29, 2026
48a5305
Disable calendar v5 test data incompatible with V3 schema
steveluc Jan 29, 2026
157b748
Revert list agent manifest to use TypeScript schema file
steveluc Jan 29, 2026
0ebc610
Add compiledSchemaFile field for grammar generation metadata
steveluc Jan 29, 2026
ea85784
Add fallback logic to derive .pas.json path from .ts path
steveluc Jan 29, 2026
a159567
Fix prettier formatting in commandHandlerContext
steveluc Jan 29, 2026
f1c3bf1
Implement NFA grammar priority system for match ranking
steveluc Jan 29, 2026
879888a
Add checked_wildcard paramSpec support to NFA priority system
steveluc Jan 29, 2026
dae1566
Enforce NFA priority by collecting and sorting all accepting threads
steveluc Jan 29, 2026
0201de4
Merge remote-tracking branch 'origin/main' into add-grammar-generatio…
steveluc Jan 30, 2026
1a33a00
Apply prettier formatting
steveluc Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion ts/packages/actionGrammar/src/agentGrammarRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ export class AgentGrammar {
* They are merged as alternatives to enable cache hits on similar requests.
*
* @param agrText Grammar rules in .agr format (from grammarGenerator)
* @param checkedVariables Optional set of variable names with validation (checked_wildcard paramSpec)
* @returns Success status and any errors
*/
addGeneratedRules(agrText: string): {
addGeneratedRules(
agrText: string,
checkedVariables?: Set<string>,
): {
success: boolean;
errors: string[];
unresolvedEntities?: string[];
Expand All @@ -71,6 +75,11 @@ export class AgentGrammar {
};
}

// Add checked variables if provided
if (checkedVariables && checkedVariables.size > 0) {
newGrammar.checkedVariables = checkedVariables;
}

// Validate entity references
const unresolvedEntities = this.validateEntityReferences(newGrammar);
if (unresolvedEntities.length > 0) {
Expand All @@ -95,6 +104,19 @@ export class AgentGrammar {
mergedGrammar.entities = Array.from(existingEntities);
}

// Merge checked variables
if (newGrammar.checkedVariables || this.grammar.checkedVariables) {
const existingChecked = new Set(
this.grammar.checkedVariables || [],
);
if (newGrammar.checkedVariables) {
for (const varName of newGrammar.checkedVariables) {
existingChecked.add(varName);
}
}
mergedGrammar.checkedVariables = existingChecked;
}

// Recompile NFA
try {
const newNFA = compileGrammarToNFA(
Expand Down Expand Up @@ -239,6 +261,10 @@ export interface AgentMatchResult {
matched: boolean;
agentId?: string; // Which agent matched
captures: Map<string, string | number>;
// Priority counts for sorting matches
fixedStringPartCount?: number;
checkedWildcardCount?: number;
uncheckedWildcardCount?: number;
// Debugging info
attemptedAgents?: string[];
tokensConsumed?: number | undefined;
Expand Down Expand Up @@ -390,6 +416,9 @@ export class AgentGrammarRegistry {
matched: true,
agentId: id,
captures: result.captures,
fixedStringPartCount: result.fixedStringPartCount,
checkedWildcardCount: result.checkedWildcardCount,
uncheckedWildcardCount: result.uncheckedWildcardCount,
attemptedAgents,
tokensConsumed: result.tokensConsumed,
};
Expand Down
36 changes: 35 additions & 1 deletion ts/packages/actionGrammar/src/generation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ import { ClaudeGrammarGenerator, GrammarAnalysis } from "./grammarGenerator.js";
import { loadSchemaInfo } from "./schemaReader.js";
import { GrammarTestCase } from "./testTypes.js";

/**
* Convert plural parameter names to singular for grammar variable names
* e.g., "artists" -> "artist"
*/
function getSingularVariableName(paramName: string): string {
if (paramName.endsWith("s") && paramName.length > 1) {
return paramName.slice(0, -1);
}
return paramName;
}

/**
* Cache population API for agentServer integration
*/
Expand Down Expand Up @@ -66,6 +77,8 @@ export interface CachePopulationResult {
success: boolean;
// The generated grammar rule text (if successful)
generatedRule?: string;
// Checked variable names (parameters with checked_wildcard paramSpec)
checkedVariables?: Set<string>;
// Reason for rejection (if not successful)
rejectionReason?: string;
// The grammar analysis performed
Expand Down Expand Up @@ -117,12 +130,33 @@ export async function populateCache(
schemaInfo,
);

return {
// Extract checked variables from the action parameters
const checkedVariables = new Set<string>();
const actionInfo = schemaInfo.actions.get(testCase.action.actionName);
if (actionInfo) {
for (const [paramName, paramInfo] of actionInfo.parameters) {
if (paramInfo.paramSpec === "checked_wildcard") {
// Handle array parameters (convert plural to singular)
const varName = Array.isArray(
testCase.action.parameters[paramName],
)
? getSingularVariableName(paramName)
: paramName;
checkedVariables.add(varName);
}
}
}

const result: CachePopulationResult = {
success: true,
generatedRule: grammarRule,
analysis,
warnings: [],
};
if (checkedVariables.size > 0) {
result.checkedVariables = checkedVariables;
}
return result;
} catch (error) {
return {
success: false,
Expand Down
90 changes: 90 additions & 0 deletions ts/packages/actionGrammar/src/grammarMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import fs from "fs";
import {
fromJSONParsedActionSchema,
ParsedActionSchemaJSON,
ActionSchemaTypeDefinition,
} from "@typeagent/action-schema";
import { Grammar } from "./grammarTypes.js";

/**
* Enrich a Grammar with checked variable metadata from a .pas.json schema file
*
* Reads the schema file to find parameters with checked_wildcard paramSpec
* and adds their variable names to grammar.checkedVariables
*
* @param grammar The grammar to enrich
* @param schemaPath Path to the .pas.json file
* @returns The enriched grammar (modifies in place and returns for chaining)
*/
export function enrichGrammarWithCheckedVariables(
grammar: Grammar,
schemaPath: string,
): Grammar {
try {
const jsonContent = fs.readFileSync(schemaPath, "utf8");
const json: ParsedActionSchemaJSON = JSON.parse(jsonContent);
const parsedSchema = fromJSONParsedActionSchema(json);

const checkedVariables = new Set<string>();

// Process each action to find parameters with checked_wildcard paramSpec
for (const [
_actionName,
actionDef,
] of parsedSchema.actionSchemas.entries()) {
const paramSpecs = (actionDef as ActionSchemaTypeDefinition)
.paramSpecs;

if (!paramSpecs || typeof paramSpecs !== "object") {
continue;
}

// Find all parameters with checked_wildcard paramSpec
for (const [paramName, paramSpec] of Object.entries(paramSpecs)) {
if (paramSpec === "checked_wildcard") {
// Add the parameter name as a checked variable
// Also handle array element specs (e.g., "artists.*" -> "artist")
if (paramName.endsWith(".*")) {
const baseName = paramName.slice(0, -2);
// Convert plural to singular for grammar variable names
const singularName = getSingularVariableName(baseName);
checkedVariables.add(singularName);
} else {
checkedVariables.add(paramName);
}
}
}
}

// Add to grammar
if (checkedVariables.size > 0) {
grammar.checkedVariables = checkedVariables;
}

return grammar;
} catch (error) {
// If schema file doesn't exist or can't be read, return grammar unchanged
console.warn(
`Could not read schema file ${schemaPath} for checked variable metadata: ${error}`,
);
return grammar;
}
}

/**
* Convert plural parameter names to singular for grammar variable names
* e.g., "artists" -> "artist", "devices" -> "device"
*/
function getSingularVariableName(plural: string): string {
if (plural.endsWith("ies")) {
return plural.slice(0, -3) + "y";
} else if (plural.endsWith("ses") || plural.endsWith("shes")) {
return plural.slice(0, -2);
} else if (plural.endsWith("s")) {
return plural.slice(0, -1);
}
return plural;
}
1 change: 1 addition & 0 deletions ts/packages/actionGrammar/src/grammarTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type GrammarRule = {
export type Grammar = {
rules: GrammarRule[];
entities?: string[] | undefined; // Entity types this grammar depends on (e.g. ["Ordinal", "CalendarDate"])
checkedVariables?: Set<string> | undefined; // Variable names with validation (checked_wildcard paramSpec)
};

/**
Expand Down
14 changes: 12 additions & 2 deletions ts/packages/actionGrammar/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,19 @@ export {
} from "./dynamicGrammarLoader.js";

// NFA system
export type { NFA, NFAState, NFATransition } from "./nfa.js";
export { matchNFA, type NFAMatchResult } from "./nfaInterpreter.js";
export type {
NFA,
NFAState,
NFATransition,
AcceptStatePriorityHint,
} from "./nfa.js";
export {
matchNFA,
sortNFAMatches,
type NFAMatchResult,
} from "./nfaInterpreter.js";
export { compileGrammarToNFA } from "./nfaCompiler.js";
export { enrichGrammarWithCheckedVariables } from "./grammarMetadata.js";

// Agent Grammar Registry
export type { AgentMatchResult } from "./agentGrammarRegistry.js";
Expand Down
38 changes: 36 additions & 2 deletions ts/packages/actionGrammar/src/nfa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,24 @@ export interface NFATransition {
// For wildcard transitions: metadata about the variable
variable?: string | undefined;
typeName?: string | undefined;
checked?: boolean | undefined; // true if wildcard has validation (entity type or checked_wildcard paramSpec)

// Target state
to: number;
}

/**
* Priority hint for an accepting state
* Used when multiple grammar rules share an accepting state (e.g., in DFA construction)
* Tracks the best-case priority achievable through this state
*/
export interface AcceptStatePriorityHint {
// Best achievable counts for any path leading to this state
minFixedStringPartCount: number; // Highest fixed string count from any rule
maxCheckedWildcardCount: number; // Most checked wildcards from any rule
minUncheckedWildcardCount: number; // Fewest unchecked wildcards from any rule
}

/**
* An NFA state with outgoing transitions
*/
Expand All @@ -43,6 +56,10 @@ export interface NFAState {
// If true, this is an accepting/final state
accepting: boolean;

// Optional: Priority hint for accepting states (used in DFA minimization/merging)
// When multiple rules merge into one accepting state, this tracks the best possible priority
priorityHint?: AcceptStatePriorityHint | undefined;

// Optional: capture variable value when reaching this state
capture?:
| {
Expand Down Expand Up @@ -88,12 +105,20 @@ export class NFABuilder {
tokens?: string[],
variable?: string,
typeName?: string,
checked?: boolean,
): void {
const state = this.states[from];
if (!state) {
throw new Error(`State ${from} does not exist`);
}
state.transitions.push({ type, to, tokens, variable, typeName });
state.transitions.push({
type,
to,
tokens,
variable,
typeName,
checked,
});
}

addTokenTransition(from: number, to: number, tokens: string[]): void {
Expand All @@ -109,8 +134,17 @@ export class NFABuilder {
to: number,
variable: string,
typeName?: string,
checked?: boolean,
): void {
this.addTransition(from, to, "wildcard", undefined, variable, typeName);
this.addTransition(
from,
to,
"wildcard",
undefined,
variable,
typeName,
checked,
);
}

build(startState: number, name?: string): NFA {
Expand Down
Loading
Loading