Partial results
You can set limits on time or cost using guards(/guide/guards). Suppose you ask a research agent to research five topics and use a guard to limit its time to 10 minutes. If the guard trips, all of the research will get thrown away. This is wasteful. If the agent researched 4/5 topics, you should be able to get that. Partial results let you save your work as you go, so if the code is aborted, you can still get something back.
There are two tools:
saveDraftsaves a value as your work improves.finalizecomputes a value at the moment the work stops.
Saving as you go with saveDraft
Here is an example without saveDraft. This code has a timeout of one second, but it sleeps for two seconds.
node main() {
const res = guard(time: 1s) {
sleep(2s)
return "finalResult"
}
print(res)
}Running this returns a failure. Now lets use saveDraft:
node main() {
const res = guard(time: 1s) {
saveDraft("draft1")
sleep(2s)
return "finalResult"
}
print(res)
}Now this returns a success with the value "draft1". The guard tripped, but the last saved draft was returned instead of a failure.
For the earlier research example:
node main() {
const result = guard(time: 10m) {
let report = ""
for (topic in topics) {
report = report + research(topic)
saveDraft(report)
}
return report
}
// If the code finished in time, `result` will have the full report.
// Otherwise, it will still contain the report so far.
if (isSuccess(result)) {
print(result.value)
}
}The loop above is deliberate: saveDraft has to run after each topic, and a comprehension body is a single expression. When you only want the results and do not need to save progress between them, a comprehension is shorter:
const reports = fork [research(topic) for topic in topics]Things to note:
- The last saved value wins.
- The draft should match the return type of your function.
- You can't use
saveDraftfrom the global scope. - If your function raises an exception, that exception gets converted to a failure in Agency. In that case, we don't return the saved draft value, we return the failure, so that it doesn't get lost.
Computing the partial with finalize
Sometimes the best partial result is not a value you saved along the way. It is something you compute from whatever you have at the moment the guard trips. A finalize block runs when the scope is aborted, and lets you salvage your work. Here is an example:
def research(topic: string): string {
const outline = draftOutline(topic)
const full = expand(outline) // guard trips in here
return full
finalize {
if (outline != null) {
return "OUTLINE ONLY: " + outline
}
return "nothing yet"
}
}Things to note:
- A finalize only runs when the scope is aborted.
- Inside a finalize, you can access any variables in scope, but they might not have been assigned yet, so each var could be null.
- You can't raise an interrupt in
finalize. - You can only have one finalize per function or block, and the convention is to put it at the end of the function.
- In a function with a finalize, a
returnexpression that contains a call must be just the call, likereturn f(x). Anything more complex is a compile error. Assign to a local first. - When both exist, finalize wins: if a scope has both a saved draft and a finalize, the finalize's return value is what gets returned.
Partials in a call chain
Example code:
def add(a: number, b: number): number {
saveDraft(a + b + 100)
sleep(2s)
return a + b
}
node main() {
const res = guard(time: 1s) {
saveDraft(1)
const x = add(1, 2)
return 2
finalize {
return x * 2
}
}
print(res)
}What's happening:
- we set a 1 second guard in
main - we save a draft of
1inmain - we call
add, which saves a draft of103and then sleeps for 2 seconds - the guard trips while
addis sleeping - we return the saved draft of
103fromadd, settingxto103inmain - the
finalizeblock inmainruns - the
finalizeblock returnsx * 2, which is206.
Notice that the guard aborts multiple functions and thus triggers partial results from multiple levels.
If main didn't have a finalize or a saved draft, it would return a failure. It doesn't get to use add's saved draft as its own.
Partials in forks and races
Concurrent work follows the same rule, per branch:
- Inside a
fork, each branch keeps its own draft value. - A
raceloser is cancelled, not tripped, so its draft is discarded, and its finalize does not run.
Partials in an interrupt payload
When a guard trips, it raises an interrupt. The interrupt's data includes draftValue:
handle {
// some code
} with (i) {
if (i.effect == "std::guard") {
if (i.data.draftValue != null) {
// Something useful exists. Stop here and keep it.
return reject()
}
// Nothing salvageable yet. Buy more time.
return approve({ maxTime: 60000 })
}
return pass()
}