Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions ts/packages/agents/browser/src/agent/commerce/translator.mts
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,12 @@ export class ECommerceSiteAgent<T extends object> {
undefined,
fastModelName,
);
this.model = ai.createChatModel(apiSettings, undefined, undefined, [
"commerce",
]);
this.model = ai.createChatModel(
apiSettings,
{ temperature: 1 },
undefined,
["commerce"],
);
const validator = createTypeScriptJsonValidator<T>(
this.schema,
schemaName,
Expand Down
9 changes: 6 additions & 3 deletions ts/packages/agents/browser/src/agent/crossword/translator.mts
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,12 @@ export class CrosswordPageTranslator<T extends object> {
undefined,
fastModelName,
);
this.model = ai.createChatModel(apiSettings, undefined, undefined, [
"crossword",
]);
this.model = ai.createChatModel(
apiSettings,
{ temperature: 1 },
undefined,
["crossword"],
);

const validator = createTypeScriptJsonValidator<T>(
this.schema,
Expand Down
9 changes: 6 additions & 3 deletions ts/packages/agents/browser/src/agent/discovery/translator.mts
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,12 @@ export class SchemaDiscoveryAgent<T extends object> {
undefined,
fastModelName,
);
this.model = ai.createChatModel(apiSettings, undefined, undefined, [
"schemaDiscovery",
]);
this.model = ai.createChatModel(
apiSettings,
{ temperature: 1 },
undefined,
["schemaDiscovery"],
);
const validator = createTypeScriptJsonValidator<T>(
this.userActionsPoolSchema,
schemaName,
Expand Down
95 changes: 87 additions & 8 deletions ts/packages/dispatcher/dispatcher/src/reasoning/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,16 +399,83 @@ async function executeReasoningWithTracing(
);

if (plan && planGenerator.validatePlan(plan)) {
await planLibrary.savePlan(plan);
debug(
`Generated and saved workflow plan: ${plan.planId} (${plan.intent})`,
// Check for duplicate plans before saving
const existingPlans = await planLibrary.findMatchingPlans(
originalRequest,
plan.intent,
);

// Notify user that a plan was created
context.actionIO.appendDisplay({
type: "text",
content: `\n✓ Created reusable workflow plan: ${plan.description}`,
});
let isDuplicate = false;
let duplicatePlanId: string | undefined;

if (existingPlans.length > 0) {
// Use PlanMatcher to check if this plan is essentially a duplicate
const planMatcher = new PlanMatcher(planLibrary);

for (const existingPlan of existingPlans) {
// Check if existing plan is user-approved
if (existingPlan.approval?.status === "approved") {
debug(
`Found user-approved plan: ${existingPlan.planId}, skipping new plan creation`,
);

// Update usage of approved plan instead
await planLibrary.updatePlanUsage(
existingPlan.planId,
true,
tracer.getTrace().metrics.duration,
);

isDuplicate = true;
duplicatePlanId = existingPlan.planId;
break;
}

// Check if the descriptions are very similar
const similarity =
await planMatcher.computeSimilarity(
plan.description,
existingPlan.description,
);

if (similarity >= 0.8) {
isDuplicate = true;
duplicatePlanId = existingPlan.planId;
debug(
`Detected duplicate plan (similarity: ${similarity}): ${existingPlan.planId}`,
);

// Update the existing plan's usage count
await planLibrary.updatePlanUsage(
existingPlan.planId,
true,
tracer.getTrace().metrics.duration,
);
break;
}
}
}

if (isDuplicate) {
debug(
`Skipped creating duplicate plan, updated existing: ${duplicatePlanId}`,
);
context.actionIO.appendDisplay({
type: "text",
content: `\n✓ Updated existing workflow plan usage (prevented duplicate)`,
});
} else {
await planLibrary.savePlan(plan);
debug(
`Generated and saved workflow plan: ${plan.planId} (${plan.intent})`,
);

// Notify user that a plan was created
context.actionIO.appendDisplay({
type: "text",
content: `\n✓ Created reusable workflow plan: ${plan.description}`,
});
}
}
} catch (error) {
// Don't fail the request if plan generation fails
Expand Down Expand Up @@ -494,6 +561,14 @@ async function executeReasoningWithPlanning(
content: `\n✓ Workflow completed successfully`,
});

// Prompt for review if plan is pending
if (match.plan.approval?.status === "pending_review") {
context.actionIO.appendDisplay({
type: "text",
content: `\n💡 This workflow is ready for review.`,
});
}

return executionResult.finalOutput
? createActionResultNoDisplay(executionResult.finalOutput)
: undefined;
Expand All @@ -518,6 +593,10 @@ async function executeReasoningWithPlanning(
}
} else {
debug("No matching plan found, using reasoning");
displayStatus(
"No matching workflow found, using reasoning...",
context,
);
}
} catch (error) {
debug("Plan matching/execution failed:", error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export class PlanGenerator {
lastUsed: new Date().toISOString(),
avgDuration: trace.metrics.duration,
},
approval: {
status: "auto",
reviewHistory: [],
},
};

debug(`Generated plan: ${plan.planId} (${plan.intent})`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,48 +62,79 @@ export class PlanLibrary {
}

/**
* Find matching plans by intent or keywords
* Find matching plans by intent or keywords (with scores)
*/
async findMatchingPlans(
async findMatchingPlansWithScores(
request: string,
intent?: string,
): Promise<WorkflowPlan[]> {
): Promise<Array<{ plan: WorkflowPlan; score: number }>> {
try {
// Load index
const index = await this.loadIndex();

if (index.plans.length === 0) {
debug("No plans in index");
return [];
}

debug(`Total plans in index: ${index.plans.length}`);

// Filter by intent if provided
let candidatePlans = intent
? index.plans.filter((p) => p.intent === intent)
: index.plans;

if (candidatePlans.length === 0) {
debug(`No plans found with intent: ${intent}`);
return [];
}

debug(
`Candidate plans after intent filter: ${candidatePlans.length}`,
);

// Rank by keyword match and usage stats
const ranked = this.rankPlans(candidatePlans, request);

// Load full plan data for top matches (up to 3)
const matches: WorkflowPlan[] = [];
debug(`Top 3 ranked plans:`);
for (let i = 0; i < Math.min(3, ranked.length); i++) {
const entry = ranked[i] as any;
debug(
` ${i + 1}. ${entry.planId} (${entry.intent}) - score: ${entry.score?.toFixed(3)}`,
);
}

// Load full plan data for top matches (up to 3) and include scores
const matches: Array<{ plan: WorkflowPlan; score: number }> = [];
for (const entry of ranked.slice(0, 3)) {
const plan = await this.loadPlan(entry.planId);
if (plan) {
matches.push(plan);
matches.push({
plan,
score: (entry as any).score || 0,
});
}
}

debug(`Returning ${matches.length} candidate plans for validation`);
return matches;
} catch (error) {
debug(`Failed to find matching plans:`, error);
return [];
}
}

/**
* Find matching plans by intent or keywords
*/
async findMatchingPlans(
request: string,
intent?: string,
): Promise<WorkflowPlan[]> {
const results = await this.findMatchingPlansWithScores(request, intent);
return results.map((r) => r.plan);
}

/**
* Update plan usage stats
*/
Expand All @@ -116,6 +147,14 @@ export class PlanLibrary {
const plan = await this.loadPlan(planId);
if (!plan) return;

// Check if plan is user-approved (immutable structure)
if (plan.approval?.status === "approved") {
debug(
`Plan ${planId} is user-approved, only updating usage stats`,
);
}

// Initialize usage if not exists
if (!plan.usage) {
plan.usage = {
successCount: 0,
Expand All @@ -125,6 +164,15 @@ export class PlanLibrary {
};
}

// Initialize approval if not exists
if (!plan.approval) {
plan.approval = {
status: "auto",
reviewHistory: [],
};
}

// Update usage stats
if (success) {
plan.usage.successCount++;
} else {
Expand All @@ -139,6 +187,16 @@ export class PlanLibrary {
(plan.usage.avgDuration * (totalExecutions - 1) + duration) /
totalExecutions;

// Mark for review after 3+ successful executions (if still auto)
if (
plan.approval.status === "auto" &&
plan.usage.successCount >= 3 &&
success
) {
plan.approval.status = "pending_review";
debug(`Plan ${planId} marked for user review`);
}

await this.savePlan(plan);

debug(
Expand Down Expand Up @@ -274,6 +332,7 @@ export class PlanLibrary {
: 0,
lastUsed: plan.usage?.lastUsed || plan.createdAt,
executionCount: totalExecutions,
approvalStatus: plan.approval?.status || "auto",
});

// Save updated index
Expand Down Expand Up @@ -322,6 +381,7 @@ export class PlanLibrary {
: 0,
lastUsed: plan.usage?.lastUsed || plan.createdAt,
executionCount: totalExecutions,
approvalStatus: plan.approval?.status || "auto",
});

// Save to instance storage
Expand Down Expand Up @@ -368,6 +428,7 @@ export class PlanLibrary {

return text
.toLowerCase()
.replace(/[^\w\s]/g, " ") // Remove punctuation
.split(/\s+/)
.filter((w) => w.length > 3 && !commonWords.has(w))
.slice(0, 10);
Expand All @@ -383,6 +444,7 @@ export class PlanLibrary {
const requestWords = new Set(
request
.toLowerCase()
.replace(/[^\w\s]/g, " ") // Remove punctuation
.split(/\s+/)
.filter((w) => w.length > 3),
);
Expand All @@ -406,11 +468,29 @@ export class PlanLibrary {
(1000 * 60 * 60 * 24);
const recencyScore = Math.exp(-daysSinceUse / 30); // 30-day half-life

// Combined score
// Approval boost
let approvalBoost = 0;
switch (plan.approvalStatus) {
case "approved":
approvalBoost = 0.3; // Significant boost for user-approved
break;
case "reviewed":
approvalBoost = 0.1; // Small boost for reviewed
break;
case "pending_review":
approvalBoost = 0.05; // Tiny boost for pending
break;
case "auto":
default:
approvalBoost = 0;
}

// Combined score (keyword: 40%, success: 25%, recency: 15%, approval: 20%)
const score =
keywordScore * 0.5 +
successWeight * 0.3 +
recencyScore * 0.2;
keywordScore * 0.4 +
successWeight * 0.25 +
recencyScore * 0.15 +
approvalBoost;

return { ...plan, score };
})
Expand Down
Loading