Skip to content

Commit

Permalink
Add agent option to abort planning with timeout
Browse files Browse the repository at this point in the history
This prevents a large task database from searching forever without
finding a suitable plan and without making progress. If that happens
that means the knowledge database should be optimized using some
additional methods

Change-type: minor
  • Loading branch information
pipex committed Jul 24, 2024
1 parent 696993c commit b22c7b6
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 17 deletions.
1 change: 1 addition & 0 deletions lib/agent/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type AgentRuntimeEvent<TState = unknown> =
}
| { event: 'plan-found'; start: PlanNode<TState>; stats: PlanningStats }
| { event: 'plan-not-found'; cause: unknown; stats: PlanningStats }
| { event: 'plan-timeout'; timeout: number }
| { event: 'backoff'; tries: number; delayMs: number }
| { event: 'success' }
| { event: 'failure'; cause: unknown }
Expand Down
1 change: 1 addition & 0 deletions lib/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ function from<TState>({
const opts: AgentOpts<TState> = {
maxRetries: Infinity,
follow: false,
plannerMaxWaitMs: 60 * 1000,
maxWaitMs: 5 * 60 * 1000,
minWaitMs: 1 * 1000,
backoffMs: (failures) => 2 ** failures * opts.minWaitMs,
Expand Down
52 changes: 35 additions & 17 deletions lib/agent/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { setTimeout as delay } from 'timers/promises';
import type { Operation } from '../operation';
import type { ReadOnly } from '../readonly';
import type { Observer, Subscription } from '../observable';
import type { PlanAction, Planner, PlanNode } from '../planner';
import type { Plan, PlanAction, Planner, PlanNode } from '../planner';
import { Ref } from '../ref';
import type { Sensor } from '../sensor';
import type { StrictTarget } from '../target';
Expand Down Expand Up @@ -77,6 +77,12 @@ class PlanNotFound extends Error {
}
}

class PlanningTimeout extends Error {
constructor(public timeout: number) {
super(`Planning aborted after ${timeout}(ms)`);
}
}

export class Runtime<TState> {
private promise: Promise<Result<TState>> = Promise.resolve({
success: false,
Expand Down Expand Up @@ -106,8 +112,8 @@ export class Runtime<TState> {
return this.stateRef._;
}

private findPlan() {
const { trace } = this.opts;
private findPlan(): Promise<Plan<TState> & { success: true }> {
const { trace, plannerMaxWaitMs } = this.opts;

let target: Target<TState>;
if (this.strict) {
Expand All @@ -127,20 +133,28 @@ export class Runtime<TState> {
});

// Trigger a plan search
const result = this.planner.findPlan(this.stateRef._, this.target);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new PlanningTimeout(plannerMaxWaitMs));
}, plannerMaxWaitMs);

if (!result.success) {
trace({
event: 'plan-not-found',
stats: result.stats,
cause: result.error,
});
throw new PlanNotFound(result.error);
}
const result = this.planner.findPlan(this.stateRef._, this.target);
clearTimeout(timer);

if (!result.success) {
trace({
event: 'plan-not-found',
stats: result.stats,
cause: result.error,
});
reject(new PlanNotFound(result.error));
return;
}

// trace event: plan found
// data, iterations, time, plan
return result;
// trace event: plan found
// data, iterations, time, plan
resolve(result);
});
}

private updateSensors(changedPath: Path) {
Expand Down Expand Up @@ -302,7 +316,7 @@ export class Runtime<TState> {
trace({ event: 'start', target: this.target });
while (!this.stopped) {
try {
const result = this.findPlan();
const result = await this.findPlan();
const { start } = result;

// The plan is empty, we have reached the goal
Expand Down Expand Up @@ -332,6 +346,10 @@ export class Runtime<TState> {
found = false;
if (e instanceof PlanNotFound) {
// nothing to do
} else if (e instanceof PlanningTimeout) {
// planning timed-out but we can keep searching as the
// state could be updated by a sensor
trace({ event: 'plan-timeout', timeout: e.timeout });
} else if (e instanceof ActionError || e instanceof PlanRunFailed) {
// nothing to do
} else if (e instanceof Cancelled) {
Expand Down Expand Up @@ -360,7 +378,7 @@ export class Runtime<TState> {
trace({ event: 'backoff', tries, delayMs: wait });
await delay(wait);

// Only backof if we haven't been able to reach the target
// Only backoff if we haven't been able to reach the target
tries += +!found;
}

Expand Down
6 changes: 6 additions & 0 deletions lib/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export interface AgentOpts<TState> {
*/
follow: boolean;

/**
* Abort search if a planning solution has not been found in this time
* Default is 60 seconds.
*/
plannerMaxWaitMs: number;

/**
* The maximum number of attempts for reaching the target before giving up. Defaults to
* infinite tries.
Expand Down
6 changes: 6 additions & 0 deletions lib/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ export function readableTrace<S = any>(logger: Partial<Logger>): Trace<S> {
}
break;

case 'plan-timeout':
log.error(
`planning timed-out after ${e.timeout}(ms), this might be a bug`,
);
return;

case 'plan-executed': {
log.info('plan executed successfully');
return;
Expand Down

0 comments on commit b22c7b6

Please sign in to comment.