Φτιάξτε ένα agent harness: το loop και οι πέντε έξοδοί του
Ένα loop δεκαπέντε γραμμών που πετυχαίνει με την πρώτη, μετά σπάει επίτηδες επτά φορές — με runaway κόστος 77×.
Σε αυτή τη σελίδα
Ξεκινήστε από το ειλικρινές κομμάτι, επειδή κανείς άλλος δεν θα το πει: το "harness" είναι jargon, όχι πρότυπο. Δεν υπάρχει προδιαγραφή, δεν υπάρχει επιτροπή, δεν υπάρχει ορισμός αναφοράς. Τα τέσσερα papers που παραθέτει αυτό το κεφάλαιο — ReAct,1 CoALA,2 SWE-bench και vLLM — δεν χρησιμοποιούν τη λέξη ούτε μία φορά στις περιλήψεις τους. Η πιο πολυκατεβασμένη υλοποίηση του πράγματος, το πακέτο ai της Vercel με 89,4 εκατομμύρια downloads τον μήνα, επίσης δεν τη χρησιμοποιεί: το string harness εμφανίζεται μηδέν φορές στα 397 KB των type declarations που διανέμει η έκδοση 7.0.93.3 Το ένα σημείο όπου η λέξη όντως σηκώνει βάρος σημαίνει κάτι εντελώς άλλο. Το SWE-bench λέει "harness" πέντε φορές στο README του, πάντα ως evaluation harness — το containerised scaffold που εφαρμόζει ένα patch και τρέχει τα tests — και το Python module του είναι κυριολεκτικά swebench.harness.run_evaluation.4
Άρα δύο διαφορετικά πράγματα μοιράζονται ένα όνομα. Ένα evaluation harness κρατά το agent ακίνητο και το βαθμολογεί. Ένα agent harness είναι το πρόγραμμα που τρέχει το agent: καλεί το model, εκτελεί ό,τι ζητά το model, αποφασίζει πότε να σταματήσει και κρατά το state ενδιάμεσα. Αυτό το κεφάλαιο χτίζει το δεύτερο, σε λιγότερες από διακόσιες γραμμές TypeScript, χωρίς κανένα framework.
Το ίδιο το loop είναι δεκαπέντε γραμμές και λειτουργεί με την πρώτη προσπάθεια. Όλα μετά από αυτό είναι ένας τρόπος να φύγετε από αυτό.
Εμφάνιση λεπτομερειών
Τι χρειάζεται αυτό το κεφάλαιο από τα προηγούμενα.
- Κεφάλαιο 14 για τον client: deadlines, status triage, cancellation, idempotency keys και την τεχνική mock provider που χρησιμοποιείται ξανά εδώ.
- Κεφάλαιο 16 για την αριθμητική: τα input tokens αυξάνονται με το τετράγωνο της συζήτησης, και οι τιμές που χρησιμοποιούνται παρακάτω είναι αυτές που διαβάστηκαν εκεί στις 6 Σεπτεμβρίου 2026.
- Κεφάλαιο 18 για τον κατάλογο εργαλείων: ένα schema που βλέπει το model, ένα endpoint που δεν βλέπει ποτέ, και τον κανόνα ότι τα σφάλματα είναι context και όχι exceptions.
- Κεφάλαιο 22 για το loop που κληρονομεί αυτό εδώ, και για τους δύο δημοσιευμένους ορισμούς του "agent" που διαφωνούν μεταξύ τους.
Δεν έχει tensors εδώ. Αυτό είναι το δεύτερο dependency hub του μαθήματος: τα Κεφάλαια 24, 25, 29 και 30 τρέχουν πάνω στο αρχείο παρακάτω, και τα 26 έως 28 χτίζουν πάνω σε όσα μπορεί να φτάσει.
Ένας provider που μπορείτε να κάνετε script
Σύνδεσμος στην ενότητα: Ένας provider που μπορείτε να κάνετε scriptΤο Κεφάλαιο 14 δεν μπορούσε να γραφτεί απέναντι σε πραγματικό provider, επειδή δεν μπορείτε να ζητήσετε από έναν 429 σε επιλεγμένη στιγμή. Αυτό το κεφάλαιο έχει το ίδιο πρόβλημα με άλλη μορφή: δεν μπορείτε να ζητήσετε από ένα πραγματικό model να ξεφύγει, ή να ζητήσει το ίδιο εργαλείο δύο φορές στη σειρά, κατά παραγγελία και αναπαραγώγιμα.
Έτσι το πρώτο πρόγραμμα είναι ένας scripted provider: ένα endpoint με το σχήμα ενός chat completions API του οποίου η απάντηση είναι συνάρτηση του δείκτη turn και του τι έχουν επιστρέψει τα εργαλεία μέχρι τώρα. Μετρά token με πραγματικό byte-pair encoder, άρα τα χρήματα παρακάτω είναι αριθμητική και όχι διακόσμηση.
const SCRIPTS = {
// A well-behaved task: list, read, answer.
plan: (t) =>
t === 0 ? asks(call("c1", "list_files", {}))
: t === 1 ? asks(call("c2", "read_file", { path: "errors.log" }))
: text("errors.log mentions a timeout: worker 7 timed out after 30000 ms."),
// Never declares itself done.
runaway: (t) => asks(call(`c${t}`, "list_files", {})),
// Guesses a file name, then corrects itself IF it was told what happened.
recover: (t, all) =>
t === 0 ? asks(call("c1", "read_file", { path: "timeout.log" }))
: /Call list_files/.test(all)
? (t === 1 ? asks(call("c2", "list_files", {}))
: t === 2 ? asks(call("c3", "read_file", { path: "errors.log" }))
: text("errors.log mentions a timeout."))
: text("I could not read the file, so I do not know."),
};
const turn = messages.filter((m) => m.role === "assistant").length;
const toolText = messages.filter((m) => m.role === "tool").map((m) => m.content).join("\n");
const message = SCRIPTS[scenario](turn, toolText);Δύο γραμμές κουβαλούν τον σχεδιασμό. Ο δείκτης turn παράγεται από τη συζήτηση, δεν κρατιέται σε μεταβλητή, άρα ο provider είναι stateless και ένα run μπορεί να σκοτωθεί και να συνεχιστεί απέναντί του. Και το recover διαβάζει τα αποτελέσματα των εργαλείων πριν αποφασίσει: ένα scripted model που διαβάζει το δικό του transcript είναι το ελάχιστο που χρειάζεται για να μετρήσετε αν το harness του έδωσε κάτι που άξιζε να διαβαστεί.
Ο κατάλογος είναι του Κεφαλαίου 18, τέσσερα εργαλεία σε τρία αρχεία: list_files, read_file, delete_file — σημειωμένο ως needsApproval — και scan_archive, που είναι αργό επίτηδες.
Το loop που λειτουργεί
Σύνδεσμος στην ενότητα: Το loop που λειτουργείΑυτή είναι όλη η ιδέα, πριν από οποιοδήποτε από τα μέρη που την κάνουν βιώσιμη.
while (true) {
const reply = await callModel(base, messages, tools, signal);
messages.push(reply.message);
const calls = reply.message.tool_calls ?? [];
if (!calls.length) return reply.message.content;
for (const c of calls) {
const tool = byName.get(c.function.name);
const result = await tool.run(JSON.parse(c.function.arguments));
messages.push({ role: "tool", tool_call_id: c.id, name: c.function.name, content: result });
}
}Στρέψτε το στον scripted provider και κάνει ακριβώς αυτό που φαίνεται ότι κάνει:
plan, cap 20 turns=3 tools=2 in=815 out=70 cost=$0.002470 ms=89 status=completed
answer: "errors.log mentions a timeout: worker 7 timed out after 30000 ms."
per-turn prompt tokens: 204, 269, 342Τρία turns, δύο εκτελέσεις εργαλείων, ένα τέταρτο του αμερικανικού cent. Προσέξτε την τελευταία γραμμή: 204, 269, 342. Κάθε turn ξαναστέλνει όλα όσα προηγήθηκαν, που είναι ο τετραγωνικός λογαριασμός του Κεφαλαίου 16 να φτάνει σε ένα σημείο όπου κανείς δεν πληκτρολόγησε τίποτα. Το υπόλοιπο αυτού του κεφαλαίου είναι ό,τι συμβαίνει όταν αυτή η γραμμή δεν σταματά να μεγαλώνει.
Σπάσιμο ένα: η εργασία που δεν τελειώνει ποτέ
Σύνδεσμος στην ενότητα: Σπάσιμο ένα: η εργασία που δεν τελειώνει ποτέΣτρέψτε το ίδιο loop στο script runaway — ένα model που ζητά ένα εργαλείο σε κάθε turn και δεν εκπέμπει ποτέ πρόζα — και το σημειωμένο return δεν ενεργοποιείται ποτέ. Δεν υπάρχει άλλη έξοδος. Το πρόγραμμα τρέχει μέχρι να πεθάνει το process ή η πιστωτική κάρτα.
Η διόρθωση είναι μία γραμμή, είναι ο πρώτος έλεγχος που συνιστά η βιβλιογραφία,5 και όλοι τελικά τον γράφουν. Αυτό που σχεδόν κανείς δεν κάνει είναι να μετρήσει πόσο αξίζει:
| turn cap | model calls | input tokens | κόστος |
|---|---|---|---|
| 8 | 8 | 3.431 | $0.009070 |
| 20 | 20 | 16.259 | $0.038038 |
| 50 | 50 | 88.649 | $0.191098 |
| 100 | 100 | 337.299 | $0.702198 |
Διαβάστε τις δύο τελευταίες σειρές μαζί. Ο διπλασιασμός του cap από 50 σε 100 δεν διπλασίασε το κόστος· το πολλαπλασίασε επί 3,7. Τα input tokens πήγαν από 88.649 σε 337.299, συντελεστής 3,8, επειδή το turn κουβαλά κάθε προηγούμενο turn μαζί του και το σύνολο είναι . Ένα turn cap δεν είναι γραμμικός επιλογέας. Είναι επιλογέας πάνω στην τετραγωνική ρίζα της χειρότερης περίπτωσής σας, γι’ αυτό το να το ανεβάσετε από 20 σε 100 "για σιγουριά" είναι απόφαση που αξίζει να κοστολογηθεί πριν την πάρετε.
Σπάσιμο δύο: ένα cap στα turns δεν είναι cap στα χρήματα
Σύνδεσμος στην ενότητα: Σπάσιμο δύο: ένα cap στα turns δεν είναι cap στα χρήματαΤο πρόβλημα με ένα turn cap είναι ότι ένα turn δεν έχει σταθερή τιμή. Είκοσι turns πάνω σε σύντομο transcript κόστισαν $0.038 παραπάνω. Είκοσι turns με κατάλογο 200 εργαλείων, ένα σύνολο ανακτημένων εγγράφων και σαράντα μηνύματα ιστορικού κοστίζουν εκατοντάδες φορές τόσο, και το cap δεν το ξέρει. Αυτό που θέλει να οριοθετήσει ο operator είναι ο λογαριασμός.
Έτσι το loop μετρά χρήματα, χρησιμοποιώντας το computeCost του Κεφαλαίου 16 απέναντι στις τιμές που διαβάστηκαν εκεί — $2.00 ανά εκατομμύριο input tokens και $12.00 ανά εκατομμύριο output, για το model που κοστολογείται σε όλο αυτό το μάθημα:
const PRICE_IN = 2.0 / 1e6, PRICE_OUT = 12.0 / 1e6;
export const cost = (u: Usage) => u.prompt_tokens * PRICE_IN + u.completion_tokens * PRICE_OUT;
// at the top of every iteration, before asking the model anything:
if (state.turns >= opts.limits.maxTurns) return stop("max_turns_exceeded", { type: "max_turns" });
if (state.costUsd >= opts.limits.maxBudgetUsd) return stop("budget_exceeded", { type: "max_budget" });
// ...and once the reply is back, before anything else happens with it:
state.costUsd += cost(reply.usage);Το ίδιο runaway script, χωρίς κανένα turn cap, τρία budgets:
| budget | turns που έφτασε | πραγματική δαπάνη |
|---|---|---|
| $0.01 | 9 | $0.010780 |
| $0.05 | 24 | $0.051790 |
| $0.20 | 52 | $0.205398 |
Δύο πράγματα αξίζει να ονομαστούν. Πρώτον, το budget αγοράζει διαφορετικό αριθμό turns κάθε φορά, που είναι και το νόημα: οριοθετεί το πράγμα που νοιάζει τον operator και αφήνει τον αριθμό των turns να πέσει εκεί όπου τον βάζει το transcript. Δεύτερον, κάθε σειρά ξεπερνά το όριο. Το budget ήταν $0.010 και δαπανήθηκαν $0.010780, επειδή ο έλεγχος τρέχει πριν από ένα turn και η τιμή ενός turn δεν είναι γνωστή μέχρι να τελειώσει. Δεν μπορείτε να οριοθετήσετε τη δαπάνη ακριβώς· μπορείτε να την οριοθετήσετε εντός κόστους ενός turn. Πείτε το στη διεπαφή αντί να προσποιείστε, και βάλτε τον έλεγχο πριν από την κλήση ώστε η υπέρβαση να είναι ένα turn και όχι δύο.
Πέντε τρόποι να φύγετε από το loop, όχι ένας
Σύνδεσμος στην ενότητα: Πέντε τρόποι να φύγετε από το loop, όχι έναςΜέχρι τώρα το loop έχει τρεις εξόδους, και το σχήμα του υπόλοιπου κεφαλαίου είναι ορατό. Ένα production run τελειώνει ακριβώς με έναν από πέντε τρόπους, και δεν είναι παραλλαγές μεταξύ τους:
| πώς τελειώνει | ποιος αποφάσισε | τι πρέπει να κάνει ο caller |
|---|---|---|
| το model σταμάτησε να ζητά | το model | διαβάστε την απάντηση |
| turn cap | εσείς, εκ των προτέρων | αυξήστε το cap ή δεχτείτε ένα μερικό αποτέλεσμα |
| budget exhausted | εσείς, εκ των προτέρων | εγκρίνετε περισσότερα χρήματα ή δεχτείτε ένα μερικό αποτέλεσμα |
| ένα σφάλμα που δεν μπορείτε να retry | ο provider ή ένα εργαλείο | διορθώστε το deployment· αποφασίζει το triage του Κεφαλαίου 14 |
| παρενέβη άνθρωπος | ένα άτομο | περιμένετε ετυμηγορία και μετά συνεχίστε |
Η συμπίεση αυτών σε ένα boolean είναι το πιο συνηθισμένο σχεδιαστικό λάθος σε αυτό το αρχείο, και είναι ακριβό με συγκεκριμένο τρόπο: τρία από τα πέντε είναι resumable και δύο δεν είναι. Ένα agent που έφτασε το turn cap του έχει έγκυρο transcript, πραγματικό μερικό αποτέλεσμα και επόμενο βήμα· ένα agent που πήρε 401 δεν έχει τίποτα από αυτά. Άρα το harness καταγράφει τον λόγο ως data:
export type RunStatus =
| "running" | "completed" | "failed"
| "max_turns_exceeded" | "budget_exceeded" | "interrupted";
export type Interruption =
| { type: "approval"; callId: string; toolName: string; args: unknown }
| { type: "max_turns" } | { type: "max_budget" }
| { type: "cancelled"; reason: string };Σπάσιμο τρία: ένα εργαλείο αποτυγχάνει
Σύνδεσμος στην ενότητα: Σπάσιμο τρία: ένα εργαλείο αποτυγχάνειΤο Κεφάλαιο 18 τελείωσε με έναν ισχυρισμό χωρίς αριθμό: δώστε το σφάλμα ενός εργαλείου πίσω στο model ως αποτέλεσμα εργαλείου αντί να το κάνετε raise, και το model συνήθως αυτοδιορθώνεται. Να ο αριθμός.
Μία αποτυχία, τρεις πολιτικές. Το scripted model μαντεύει ένα αρχείο που δεν υπάρχει· το εργαλείο πετά no such file: timeout.log. Call list_files to see what exists.
| τι κάνει το harness με το σφάλμα | turns | tool runs | κόστος | τι πήρε ο χρήστης |
|---|---|---|---|---|
| το πετά έξω από το loop | 1 | 1 | $0.000756 | ένα stack trace |
επιστρέφει Error: the tool failed. | 2 | 1 | $0.001462 | "Δεν μπόρεσα να διαβάσω το αρχείο, άρα δεν ξέρω." |
| επιστρέφει τι συνέβη πραγματικά | 4 | 3 | $0.003550 | "Το errors.log αναφέρει timeout." |
Η τρίτη σειρά κοστίζει 4,7 φορές την πρώτη και είναι η μόνη που απαντά στην ερώτηση. Και η δεύτερη σειρά είναι η ενδιαφέρουσα, επειδή είναι αυτό που κάνουν στην πράξη οι περισσότερες codebases: το σφάλμα πιάστηκε, το loop επέζησε, το model ενημερώθηκε ότι κάτι απέτυχε και όχι τι, και τα παράτησε ευγενικά. Η διαφορά μεταξύ της δεύτερης και της τρίτης σειράς δεν είναι error handling. Είναι μια πρόταση γραμμένη για αναγνώστη.
Το harness λοιπόν αντιμετωπίζει ένα thrown tool ως data, και κάνει τη διατύπωση πολιτική:
} catch (err: any) {
if (signal.aborted) return stop("interrupted", { type: "cancelled", reason: String(signal.reason) });
if (opts.toolErrorsAreFatal) { state.error = err.message; return stop("failed"); }
result = (opts.toolErrorText ?? ((e: Error) => `Error: ${e.message}`))(err);
}Το Κεφάλαιο 18 προειδοποίησε και για την άλλη πλευρά, και έχει κι αυτή τίμημα. Στρέψτε το loop σε ένα εργαλείο που αποτυγχάνει για λόγο που κανένα μήνυμα δεν μπορεί να διορθώσει — μια ανάγνωση που το process δεν επιτρέπεται να εκτελέσει — και το model την κάνει retry για πάντα:
read a file the process may not open turns=12 toolruns=11 in=7,079 cost=$0.018622
status=max_turns_exceeded answer=""Έντεκα ίδιες εκτελέσεις μιας κλήσης που δεν μπορεί να πετύχει, 5,2 φορές το κόστος του run που ανέκαμψε από μία διορθώσιμη αποτυχία, και τίποτα στο τέλος. Τα σφάλματα είναι context· ένα μόνιμο σφάλμα είναι context που δηλητηριάζει το υπόλοιπο run. Η διάκριση είναι το status triage του Κεφαλαίου 14 μετακινημένο ένα επίπεδο πάνω: ένα σφάλμα πάνω στο οποίο μπορεί να δράσει το model επιστρέφει στο transcript, και ένα σφάλμα πάνω στο οποίο δεν μπορεί πρέπει να σταματά το run με λόγο. Το turn cap είναι αυτό που στέκεται σήμερα ανάμεσα σε εσάς και τη δεύτερη περίπτωση, που είναι πάτωμα και όχι διόρθωση.
Σπάσιμο τέσσερα: η ίδια κλήση, δύο φορές
Σύνδεσμος στην ενότητα: Σπάσιμο τέσσερα: η ίδια κλήση, δύο φορέςΤώρα η αποτυχία που οι περισσότεροι υποθέτουν ότι δεν μπορεί να συμβεί. Τα models επαναλαμβάνονται. Ζητήστε από οποιοδήποτε loop να τρέξει αρκετά και θα δείτε το ίδιο εργαλείο με τα ίδια arguments σε δύο συνεχόμενα turns.
Μετρημένο απέναντι στο baseline της ίδιας εργασίας χωρίς την επανάληψη:
| turns | tool runs | κόστος | |
|---|---|---|---|
| η εργασία, χωρίς επανάληψη | 2 | 1 | $0.001396 |
| η ίδια εργασία, μία κλήση επαναλαμβάνεται | 3 | 2 | $0.002446 |
| επαναλαμβανόμενη, με result cache σε read-only εργαλεία | 3 | 1 | $0.002446 |
Η διπλή κλήση κόστισε $0.001050 επιπλέον, αύξηση 75 %, και εδώ είναι το σημείο που εκπλήσσει: το caching του αποτελέσματος δεν ανέκτησε τίποτα από αυτό. Το deduplication έσωσε την εκτέλεση του εργαλείου και όχι το turn, επειδή μέχρι να παρατηρήσει ο κώδικάς σας την επανάληψη, το model έχει ήδη πληρωθεί για το αίτημα. Η εξοικονόμηση είναι πραγματική όταν το εργαλείο είναι αργό, rate-limited ή χρεώνεται ανά κλήση — και είναι μηδέν στο line item που μεγάλωσε.
Υπάρχει χειρότερη εκδοχή. Εφαρμόστε το ίδιο cache σε εργαλείο που γράφει, και η δεύτερη κλήση σιωπηλά δεν συμβαίνει:
naive cache on every tool 3 turns, 1 tool run, files deleted: ["access.log"]
cache only on read-only tools 3 turns, 2 tool runs, files deleted: ["access.log","access.log"]Ποιο από αυτά είναι σωστό; Κανένα, γνωστά. Το protocol λέει ότι αυτές είναι δύο κλήσεις: κουβαλούν δύο διαφορετικές τιμές tool_call_id. Τα arguments λένε ότι μπορεί να είναι μία. Ένα harness που αποφασίζει συγκρίνοντας argument strings μια μέρα θα καταπιεί τη δεύτερη από δύο ίδιες, σκόπιμες χρεώσεις — και το Κεφάλαιο 14 έχει ήδη ονομάσει τον μόνο μηχανισμό που το λύνει τίμια, που είναι ένα idempotency key που παράγεται ανά λογική λειτουργία από το επίπεδο που ξέρει τι είναι η λειτουργία. Μέχρι να κουβαλά το εργαλείο ένα τέτοιο, το υπερασπίσιμο default είναι το read-only gate παραπάνω: κάντε cache στα reads, εκτελέστε τα writes, και αφήστε το idempotency του ίδιου του write να χειριστεί τα υπόλοιπα.
if (opts.dedupe && (tool.readOnly || opts.dedupeAll) && seen.has(signature)) {
state.messages.push({ role: "tool", tool_call_id: c.id, name: c.function.name, content: seen.get(signature)! });
continue;
}Σπάσιμο πέντε: διαγράφει κάτι
Σύνδεσμος στην ενότητα: Σπάσιμο πέντε: διαγράφει κάτιΤο script destructive απαριθμεί τα αρχεία και μετά ζητά να διαγράψει ένα που η εργασία δεν ανέφερε ποτέ. Τίποτα στο loop μέχρι τώρα δεν θα το σταματούσε.
Ένα εργαλείο σημειωμένο ως needsApproval δεν αποτυγχάνει και δεν προχωρά. Σταματά το run και επιστρέφει τον έλεγχο, με όλα όσα χρειάζεται ένας άνθρωπος για να αποφασίσει:
if (tool.needsApproval && !state.approved.includes(c.id)) {
trace(state.runId, "approval_required", { toolName: tool.name, args: c.function.arguments, callId: c.id });
return stop("interrupted", { type: "approval", callId: c.id, toolName: tool.name, args: JSON.parse(c.function.arguments) });
}stopped at turn 2: interrupted / approval -> delete_file({"path":"access.log"})
files deleted so far: []
approve -> total turns=3 deleted=["access.log"] "Deleted access.log to free space."
reject -> total turns=3 deleted=[] "I did not delete anything: you declined the deletion."Αυτός είναι όλος ο μηχανισμός, και ο λόγος που είναι return αντί για callback είναι η επόμενη ενότητα: ανάμεσα στη διακοπή και την ετυμηγορία, το process μπορεί να μην υπάρχει πια.
Αλλά πρώτα, η μέτρηση που κανείς δεν περιμένει. Μια απόρριψη δεν είναι απουσία αποτελέσματος — το transcript έχει ένα slot με κλειδί tool_call_id και κάτι πρέπει να μπει μέσα. Τρέξτε την ίδια απόρριψη δύο φορές, αλλάζοντας μόνο τι λέει αυτό το κάτι:
rejected with a reason deleted=[] the agent then told the user:
"I did not delete anything: you declined the deletion."
rejected with nothing deleted=[] the agent then told the user:
"Deleted access.log to free space."Τίποτα δεν διαγράφηκε σε κανένα από τα δύο runs, και στο δεύτερο λέγεται στον χρήστη ότι διαγράφηκε. Το σύστημα δικαιωμάτων λειτούργησε τέλεια· η αναφορά είναι ψέμα. Είναι ο ίδιος μηχανισμός με τον πίνακα tool-error, φτάνοντας κάπου που έχει πολύ μεγαλύτερη σημασία — ένας άνθρωπος είπε όχι, η ενέργεια μπλοκαρίστηκε σωστά, και η σύνοψη του agent αντιφάσκει με την πραγματικότητα επειδή η άρνηση δεν γράφτηκε ποτέ εκεί όπου διαβάζει το model. Ο κανόνας που προκύπτει είναι σύντομος: ό,τι κι αν αποφασίσει ο κώδικάς σας για μια tool call, γράψτε την απόφαση στο transcript με λόγια. Το Κεφάλαιο 30 επιστρέφει σε αυτό από την πλευρά της ασφάλειας, όπου είναι η διαφορά ανάμεσα σε audit trail και μυθοπλασία.
Σπάσιμο έξι: το process πεθαίνει
Σύνδεσμος στην ενότητα: Σπάσιμο έξι: το process πεθαίνειΜια έγκριση παίρνει λεπτά ή ώρες. Ένα deploy παίρνει δευτερόλεπτα. Αν το run ζει σε μια τοπική μεταβλητή μέσα σε ένα HTTP request, κάθε restart είναι χαμένο run και κάθε έγκριση είναι race.
Άρα το run δεν είναι closure. Είναι ένα απλό serialisable object — messages, turn count, cost, status, interruption, η λίστα των approved call ids — και το loop είναι pure function πάνω του. Αυτός ο μοναδικός περιορισμός είναι που κάνει το persistence θέμα μίας γραμμής:
export const save = (s: RunState, dir: string) => writeFileSync(`${dir}/${s.runId}.json`, JSON.stringify(s));
export const load = (dir: string, runId: string) => JSON.parse(readFileSync(`${dir}/${runId}.json`, "utf8"));Το ερώτημα ορθότητας δεν είναι η αποθήκευση. Είναι τι συμβαίνει στην επιστροφή, και η αφελής απάντηση σας διπλοχρεώνει. Αν το process πέθανε αφού το model ζήτησε ένα εργαλείο αλλά πριν γραφτεί το αποτέλεσμα, ένα resume που ξεκινά καλώντας ξανά το model πληρώνει για ένα turn που ήδη έχει — και αν ξεκινά ξανατρέχοντας τα εργαλεία, εκτελεί ένα write δύο φορές.
Η διόρθωση είναι να κάνετε το loop να ξεκινά ρωτώντας το transcript τι εκκρεμεί:
export function pending(state: RunState): ToolCall[] {
const answered = new Set(state.messages.filter((m) => m.role === "tool").map((m) => m.tool_call_id));
const last = state.messages.at(-1);
if (last?.role !== "assistant") return [];
return (last.tool_calls ?? []).filter((c) => !answered.has(c.id));
}Κάθε iteration στραγγίζει πρώτα το pending και ρωτά το model μόνο όταν δεν εκκρεμεί τίποτα. Το resume γίνεται το ίδιο code path με το κανονικό, και το ίδιο και η έγκριση — μια εγκεκριμένη κλήση είναι απλώς μια pending κλήση που πλέον επιτρέπεται να τρέξει. Σκοτώστε το process στη μέση της εργασίας και επανεκκινήστε το:
process died after turn 2. tool runs so far: list_files, read_file:errors.log
restored from disk: turns=2 cost=$0.001570 messages=6 status=running
resumed and finished: turns=3 cost=$0.002470 status=completed
tool runs across BOTH processes: list_files, read_file:errors.logΔύο εκτελέσεις εργαλείων σε δύο processes για μια εργασία που χρειάζεται δύο, και το τελικό κόστος είναι ίδιο με το run που δεν κατέρρευσε ποτέ. Το κόστος συσσωρεύεται μέσα από το restart επειδή ήταν στο state, όχι σε μεταβλητή.
Σπάσιμο επτά: τρία λεπτά σιωπής
Σύνδεσμος στην ενότητα: Σπάσιμο επτά: τρία λεπτά σιωπήςΤο scan_archive παίρνει τρία δευτερόλεπτα εδώ και αντιπροσωπεύει το εργαλείο που παίρνει τρία λεπτά στην παραγωγή. Δύο πράγματα λείπουν όσο τρέχει: ο χρήστης δεν έχει ιδέα ότι συμβαίνει κάτι, και το κουμπί Stop δεν κάνει τίποτα.
Και τα δύο έχουν την ίδια διόρθωση, και είναι το AbortSignal του Κεφαλαίου 14 σπρωγμένο ένα επίπεδο βαθύτερα. Το signal δεν είναι μόνο για το fetch — περνά μέσα στο εργαλείο, και ένα καλογραμμένο εργαλείο το τιμά:
result = await tool.run(JSON.parse(c.function.arguments), {
signal,
progress: (label) => { trace(state.runId, "tool_progress", { toolName: tool.name, label }); opts.onProgress?.(label); },
});progress: scanned 200 of 1200 files (t+506 ms)
progress: scanned 400 of 1200 files (t+1007 ms)
no cancellation: stopped after 3,015 ms, status=completed
user presses Stop at 1.2 s: stopped after 1,202 ms, status=interrupted, reason="user pressed Stop"Δύο milliseconds από το κλικ μέχρι τη διακοπή, επειδή το sleep μέσα στο εργαλείο ακούει το ίδιο signal που ακούει και το fetch. Περάστε το μόνο μέσα στο fetch και το ίδιο κουμπί Stop περιμένει τρία δευτερόλεπτα — το μήκος του εργαλείου — και το run "ακυρώνεται" αφού η δουλειά που ακύρωνε έχει ήδη τελειώσει. Cancellation που δεν είναι plumbed μέχρι κάτω είναι ένα spinner που λέει τη σωστή λέξη.
Το trace, και γιατί δεν είναι log
Σύνδεσμος στην ενότητα: Το trace, και γιατί δεν είναι logΤο harness εκπέμπει μία γραμμή ανά event, και το λεξιλόγιο είναι αρκετά μικρό για να απομνημονευτεί: turn, tool_start, tool_progress, tool_result, approval_required, run_stopped.
{"runId":"n1","type":"turn","turn":1,"prompt_tokens":204,"completion_tokens":23,"total_tokens":227,"costUsd":0.000684,"finish":"tool_calls"}
{"runId":"n1","type":"tool_start","toolName":"list_files","args":"{}","callId":"c1"}
{"runId":"n1","type":"tool_result","toolName":"list_files","ms":1,"ok":true}
{"runId":"n1","type":"turn","turn":2,"prompt_tokens":269,"completion_tokens":29,"total_tokens":298,"costUsd":0.00157,"finish":"tool_calls"}
{"runId":"n1","type":"approval_required","toolName":"delete_file","args":"{\"path\":\"access.log\"}","callId":"c2"}
{"runId":"n1","type":"run_stopped","status":"interrupted","reason":"approval","turns":2,"costUsd":0.00157}Τρεις ιδιότητες το κάνουν trace και όχι logging. Κάθε γραμμή κουβαλά το run id, άρα ένα run που απλώνεται σε τρία processes και δύο ημέρες είναι ένα query. Κάθε γραμμή turn κουβαλά τα δικά της token counts και το running cost, άρα το "γιατί αυτό το run κόστισε σαράντα δολάρια" μπορεί να απαντηθεί εκ των υστέρων αντί να είναι αναπαραγώγιμο μόνο θεωρητικά. Και το run_stopped κουβαλά τον λόγο, που είναι το πεδίο που μετατρέπει ένα support ticket σε απάντηση μίας γραμμής: ένα agent που σταμάτησε στο budget και ένα agent που κατέρρευσε φαίνονται ίδια απ’ έξω και χρειάζονται αντίθετες αντιδράσεις.
Η αριθμητική του latency
Σύνδεσμος στην ενότητα: Η αριθμητική του latencyΤο Κεφάλαιο 13 μέτρησε χρόνο μέχρι το πρώτο token σε hardware που σας ανήκει. Το Κεφάλαιο 14 τον μέτρησε μέσα από socket. Ένα agent τον πολλαπλασιάζει, και ο πολλαπλασιαστής είναι ένας αριθμός που κανείς δεν διάλεξε:
Η ίδια εργασία τριών turns, αλλάζοντας μόνο το latency του provider:
| provider latency ανά turn | wall clock, 3 turns |
|---|---|
| 0 ms | 15 ms |
| 200 ms | 615 ms |
| 800 ms | 2.413 ms |
Το ίδιο το harness συνεισφέρει δεκαπέντε milliseconds σε ένα run τριών turns. Όλα τα υπόλοιπα είναι πολλαπλασιασμένο με έναν αριθμό που δεν ελέγχετε — ορισμένο μέσα σε έναν serving scheduler που κάνει batching το request σας με requests αγνώστων6 — και το επιλέγεται από το model. Γι’ αυτό το streaming του Κεφαλαίου 14 έχει μεγαλύτερη σημασία εδώ απ’ ό,τι σε ένα chat και βοηθά λιγότερο: μπορείτε να κάνετε stream το τελικό turn, και τα τέσσερα turns πριν από αυτό είναι σιωπή εκτός αν το harness εκπέμπει πρόοδο. Είναι επίσης όλο το επιχείρημα για το event tool_progress παραπάνω — σε ένα agent, η ειλικρινής μονάδα feedback δεν είναι το token, είναι το βήμα.
Το ίδιο harness, με πραγματικό model πίσω από το port
Σύνδεσμος στην ενότητα: Το ίδιο harness, με πραγματικό model πίσω από το portΌλα παραπάνω έτρεξαν απέναντι σε scripted provider, που αποδεικνύει το harness και δεν αποδεικνύει τίποτα για models. Άρα αλλάξτε μία γραμμή — το seam από το Κεφάλαιο 14, LLM_BASE_URL — και στρέψτε τον ίδιο κώδικα σε ένα τοπικό Qwen2.5-0.5B-Instruct με τα ίδια τέσσερα εργαλεία. Έξι εργασίες πάνω στα ίδια τρία αρχεία:
turns=2 tools=1 wall= 15,260ms Which file mentions a timeout? -> "The file timeout.txt does not exist..."
turns=2 tools=1 wall= 13,037ms How many files are in the directory? -> "There are three files..."
turns=2 tools=1 wall= 10,121ms Read notes.txt and tell me what it says. -> "Remember to rotate your logs."
turns=2 tools=2 wall= 21,290ms List the files and then read each one.
turns=2 tools=1 wall= 10,698ms Which file is the largest? -> "The largest file is access.log."
turns=2 tools=1 wall= 12,490ms Is there a file about rotating logs?
TOTAL turns=12 toolruns=7 wall=82,896ms mean turn=6,908msΤρία ευρήματα, και το τρίτο είναι ο λόγος που υπάρχει αυτή η ενότητα.
Κάθε μία εργασία τελείωσε σε ακριβώς δύο turns. Το turn cap δεν ενεργοποιήθηκε ποτέ, το budget δεν ενεργοποιήθηκε ποτέ, και η μόνη έξοδος του loop ήταν το model να παράγει πρόζα. Ένα model μισού δισεκατομμυρίου παραμέτρων δεν επαναλαμβάνει· απαντά στη δεύτερη ανάσα του είτε έχει ό,τι χρειάζεται είτε όχι. Το turn count είναι ιδιότητα του model, όχι του loop σας.
Το μέσο turn πήρε 6.908 milliseconds, άρα ο πίνακας latency παραπάνω δεν είναι παιχνίδι: σε αυτό το μέγεθος, ένα υποθετικό run οκτώ turns είναι σχεδόν ένα λεπτό wall clock χωρίς τίποτα στην οθόνη.
Και οι απαντήσεις είναι λάθος. Το μεγαλύτερο αρχείο είναι το errors.log· το model απαρίθμησε τα αρχεία, δεν τα διάβασε ποτέ και ονόμασε ένα έτσι κι αλλιώς. Η πρώτη εργασία μάντεψε όνομα αρχείου, ενημερώθηκε ότι δεν υπάρχει, και κατέληξε. Το harness εκτελέστηκε άψογα και στα έξι runs. Ένα harness κάνει ένα agent governable, όχι σωστό — το Κεφάλαιο 29 είναι το πώς μαθαίνετε ποιο από τα δύο συμβαίνει, και το Κεφάλαιο 30 είναι τι κοστίζει όταν κανείς δεν το έκανε.
Subagents, ονομασμένα εδώ και χρεωμένα αργότερα
Σύνδεσμος στην ενότητα: Subagents, ονομασμένα εδώ και χρεωμένα αργότεραΈνα εργαλείο στον κατάλογο μπορεί να έχει ένα άλλο run πίσω του. Η διεπαφή είναι του Κεφαλαίου 18 — ένα schema και ένα endpoint — και ένα ολόκληρο agent χωρά πίσω της επειδή αυτή η διεπαφή είναι στενή:
const research: Tool = {
name: "research",
description: "Investigate one question and return a short summary.",
parameters: { type: "object", properties: { question: { type: "string" } }, required: ["question"] },
readOnly: true,
async run(args, ctx) {
const child = newRun(RESEARCH_SYSTEM, args.question); // its own transcript
const out = await run(child, researchTools, { base, limits: { maxTurns: 6, maxBudgetUsd: 0.05 }, signal: ctx.signal });
return out.output ?? "no result";
},
};Τρία πράγματα είναι ήδη σωστά σε αυτές τις δέκα γραμμές και και τα τρία είναι συνέπειες αποφάσεων που πάρθηκαν παραπάνω: το child έχει το δικό του window, άρα το transcript του parent λαμβάνει σύνοψη αντί για όλα όσα διάβασε το child· έχει τα δικά του limits, άρα ένα runaway child δεν μπορεί να ξοδέψει το budget του parent· και κληρονομεί το signal, άρα ένα Stop ακυρώνει το δέντρο. Το γιατί ένα καθαρό window είναι το νόημα και όχι side effect είναι στο Κεφάλαιο 24· τα πέντε orchestration patterns — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — και το handoff είναι στο Κεφάλαιο 25.
Πού βρίσκονται τα frameworks, και γιατί αυτό το μάθημα δεν χρησιμοποίησε ένα
Σύνδεσμος στην ενότητα: Πού βρίσκονται τα frameworks, και γιατί αυτό το μάθημα δεν χρησιμοποίησε έναΤίποτα από τα παραπάνω δεν πρέπει να διαβαστεί ως επιχείρημα κατά των βιβλιοθηκών. Μετρημένα στις 7 Σεπτεμβρίου 2026, για τον μήνα που έληξε στις 29 Αυγούστου:7
| package | downloads εκείνου του μήνα | τι σας δίνει |
|---|---|---|
ai (Vercel AI SDK) | 89.385.860 | ToolLoopAgent, stopWhen, tool approval, step hooks |
@anthropic-ai/claude-agent-sdk | 41.558.352 | το Claude Code harness ως βιβλιοθήκη: loop, sessions, hooks, permissions, subagents8 |
@langchain/langgraph | 12.812.815 | το loop ως explicit state graph |
langchain | 11.359.058 | chains, agents, integrations |
@openai/agents | 6.093.155 | agents, handoffs, guardrails |
@mastra/core | 5.914.502 | agents, workflows, memory |
Ο λόγος που αυτό το μάθημα γράφει το loop με το χέρι αντί να διδάξει ένα από αυτά δηλώνεται αντί να υπονοείται, και είναι μετρήσιμος. Στους δώδεκα μήνες έως τις 7 Σεπτεμβρίου 2026, το ai δημοσίευσε 945 εκδόσεις και μετακινήθηκε από major 5 σε major 7, και η agent class του εξακολουθεί να εξάγεται ως Experimental_Agent· το langchain δημοσίευσε 132 εκδόσεις στο ίδιο παράθυρο· το @openai/agents δημοσίευσε 83 και είναι ακόμη στο 0.x, δεκαπέντε μήνες μετά την πρώτη του κυκλοφορία.7 Ένα κεφάλαιο γραμμένο απέναντι σε οποιοδήποτε από αυτά τα APIs μπαγιατεύει μέσα σε μία σεζόν, και αυτό εδώ δημοσιεύεται σε τριάντα τρεις γλώσσες, άρα κάθε επανέκδοση κοστίζει όλη τη μετάφραση. Αυτό που βρίσκεται κάτω από όλα τους δεν μετακινείται: ένα loop, ένας κανόνας stopping, ένας κατάλογος, ένας executor, λίγο state.
Και η reference implementation συμφωνεί με αυτό το κεφάλαιο για το κομμάτι που έχει σημασία. Στην έκδοση 7.0.93 του ai, η έξοδος του loop δεν είναι αριθμός — είναι stopWhen, μια λίστα από predicates, από τα οποία το step count είναι απλώς ένα:3
type StopCondition<TOOLS extends ToolSet> = (options: { steps: Array<StepResult<TOOLS>> }) => PromiseLike<boolean> | boolean;
declare function isStepCount(stepCount: number): StopCondition<any, any>; // exported as stepCountIsΤο stopping είναι πληθυντικό στην πιο χρησιμοποιημένη υλοποίηση αυτού του loop, για τον ίδιο λόγο που είναι πληθυντικό στις εκατόν ενενήντα έξι γραμμές παραπάνω.
Πού πάει αυτό στη συνέχεια
Σύνδεσμος στην ενότητα: Πού πάει αυτό στη συνέχειαΤώρα έχετε ένα harness: ένα loop, έναν κατάλογο, έναν executor, πέντε τρόπους εξόδου, ένα persisted run, ένα signal που φτάνει στα εργαλεία και ένα trace με run id σε κάθε γραμμή. Τα Κεφάλαια 24, 25, 29 και 30 χτίζουν πάνω σε αυτό το αρχείο, και τα 26 έως 28 πάνω σε όσα μπορεί να φτάσει.
Του μένει ένα πρόβλημα, και οι μετρήσεις παραπάνω το έδειχναν σε όλη τη διαδρομή. Κοιτάξτε άλλη μία φορά τον πίνακα runaway: 3.431 input tokens στα οκτώ turns, 337.299 στα εκατό. Κοιτάξτε το working run: 204, 269, 342. Κάθε turn ξαναστέλνει ολόκληρο το transcript, άρα το context ενός agent γεμίζει με το δικό του ιστορικό — και το model είναι χειρότερο στο να χρησιμοποιεί το μακρινό άκρο ενός μεγάλου window από το κοντινό, γι’ αυτό ένα καλό agent στο turn πέντε είναι μπερδεμένο στο turn σαράντα.
Ένα turn cap δεν το διορθώνει. Απλώς σας σταματά από το να πληρώνετε για να το βλέπετε να συμβαίνει. Αυτό που το διορθώνει είναι να αποφασίζετε, σε κάθε ένα turn, ποια tokens αξίζουν το window: τι να συμπιέσετε, τι να μετακινήσετε έξω σε μια σημείωση που μπορεί να ανακτήσει το agent, τι να δώσετε σε subagent με καθαρό window, και ποιοι ορισμοί εργαλείων αξίζουν τον μόνιμο φόρο τους. Το Κεφάλαιο 24 μετρά πού πηγαίνει πραγματικά το window — και η έκπληξη είναι ότι δεν είναι η συζήτηση.
Πηγές και μέθοδος
Σύνδεσμος στην ενότητα: Πηγές και μέθοδοςΚάθε αριθμός σε αυτό το κεφάλαιο βγήκε από τους δύο servers που περιγράφονται παραπάνω, σε Node 22 πάνω από loopback interface: ένας scripted provider που μετρά token με το encoding o200k_base, και Qwen/Qwen2.5-0.5B-Instruct πίσω από endpoint του ίδιου σχήματος, greedy decoding, σε CPU. Τα κόστη υπολογίζονται από μετρημένα token counts στις τιμές που διάβασε το Κεφάλαιο 16 στις 6 Σεπτεμβρίου 2026 — $2.00 ανά εκατομμύριο input tokens και $12.00 ανά εκατομμύριο output — και κανένα request σε αυτό το κεφάλαιο δεν πήγε σε πληρωμένο endpoint. Οι απαντήσεις του τοπικού model είναι απαντήσεις μικρού model· διαβάστε τες ως evidence για το loop, που είναι ίδιο και στις δύο περιπτώσεις, και όχι ως benchmark του τι κάνουν τα σημερινά models.
Παραπομπές
Σύνδεσμος στην ενότητα: Παραπομπές-
Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. και Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). Η εναλλαγή reasoning traces και actions που υλοποιεί το loop, και η πηγή της παρατήρησης ότι το acting επιτρέπει σε ένα model να "handle exceptions" — που είναι ακριβώς αυτό που μετρά ο πίνακας tool-error παραπάνω. ↩
-
Sumers, T. R., Yao, S., Narasimhan, K. και Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Η τυπική επεξεργασία αυτού που το loop παραπάνω κάνει άτυπα: modular memory components, ένας structured action space που εκτείνεται σε internal memory και external environments, και "a generalized decision-making process to choose actions". Διαβάστε το για το λεξιλόγιο που λείπει από τον κλαδικό όρο — ειδικά τον διαχωρισμό working, episodic, semantic και procedural memory, του οποίου η πρακτική σκιά είναι ο πίνακας three-store του Κεφαλαίου 24. ↩
-
ai(Vercel AI SDK) έκδοση 7.0.93, δημοσιευμένη στις 4 Σεπτεμβρίου 2026· τα type declarations διαβάστηκαν απόcdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.tsστις 7 Σεπτεμβρίου 2026. Το αρχείο 397 KB περιέχει μηδενικές εμφανίσεις του stringharness. Η agent class είναιdeclare class ToolLoopAgent, εξαγόμενη τόσο ωςToolLoopAgentόσο και ωςExperimental_Agent· τοdeclare function isStepCount(stepCount: number)— εξαγόμενο ωςstepCountIs— παρατίθεται κατά λέξη παραπάνω· τοtype StopConditionεμφανίζεται χωρίς τη δεύτερη type parameter του (RUNTIME_CONTEXT extends Context = Context), που είναι η μόνη παράλειψη στο απόσπασμα, όπως και το σχήμα τουstopWhen?: Arrayable<StopCondition<...>>σταgenerateTextκαιstreamText. Το ίδιο αρχείο δηλώνειtoolApproval,ToolApprovalStatus,prepareStepκαιrepairToolCall, που σημαίνει ότι η reference implementation έχει φτάσει ανεξάρτητα σε approval gates, per-step preparation και error repair. ↩ ↩2 -
Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. και Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Η περίληψη αποκαλεί το artefact "evaluation framework" 2.294 προβλημάτων και δεν χρησιμοποιεί ποτέ τη λέξη "harness"· το README του ίδιου του project (
github.com/SWE-bench/SWE-bench, αναγνώστηκε 7 Σεπτεμβρίου 2026) τη χρησιμοποιεί πέντε φορές, πάντα ως "evaluation harness", και το entry point είναιpython -m swebench.harness.run_evaluation. Αυτή είναι η άλλη σημασία της λέξης: ένα scaffold που κρατά το agent ακίνητο και το βαθμολογεί, όχι το loop που το τρέχει. ↩ -
Anthropic, Building effective agents, 19 Δεκεμβρίου 2024,
anthropic.com/engineering/building-effective-agents, αναγνώστηκε 7 Σεπτεμβρίου 2026. Το augmented model ως building block, το agent ως LLM "using tools based on environmental feedback in a loop", και η σύσταση για stopping conditions "such as a maximum number of iterations" ώστε να διατηρείται ο έλεγχος. Το Κεφάλαιο 22 παραθέτει τον ορισμό του πλήρως. ↩ -
Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. και Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). Το άλλο loop — ο serving scheduler που κάνει batching το request σας με requests αγνώστων και διαχειρίζεται το KV cache του Κεφαλαίου 13. Αξίζει να ξέρετε ότι υπάρχει ακριβώς επειδή δεν είναι δικό σας: το latency που πολλαπλασιάζει το harness σας ορίζεται μέσα του, και όσο κι αν δουλέψετε στο loop σας δεν το μετακινείτε. ↩
-
Μετρήσεις downloads του npm registry,
api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, ένα explicit window αντί για το rollinglast-month, και release histories απόregistry.npmjs.org/<package>· και τα δύο ερωτήθηκαν στις 7 Σεπτεμβρίου 2026. Τα release counts είναι ο αριθμός εκδόσεων που δημοσιεύτηκαν στους δώδεκα μήνες έως εκείνη την ημερομηνία, συμπεριλαμβανομένων canary builds:ai945 (τελευταία 7.0.93 στις 2026-09-04, με major versions 5, 6 και 7 να εμφανίζονται όλα μέσα στο window),langchain132 (τελευταία 1.5.10 στις 2026-08-20),@openai/agents83 (τελευταία 0.17.0 στις 2026-08-19, πρώτη δημοσίευση 2025-06-03). ↩ ↩2 -
Το Claude Agent SDK (
@anthropic-ai/claude-agent-sdk) είναι το Claude Code harness συσκευασμένο ως βιβλιοθήκη — agent loop, ενσωματωμένα εργαλεία file και shell, context management, sessions, hooks, permissions και subagents — τεκμηριωμένο στοcode.claude.com/docs/en/agent-sdk. Είναι το πιο κοντινό πράγμα σε δημοσιευμένη καταγραφή κάθε μηχανισμού που αυτό το κεφάλαιο χτίζει με το χέρι, και αξίζει να το διαβάσετε δίπλα στη δική σας υλοποίηση για τα μέρη που ονομάζει και αυτό το κεφάλαιο μόνο υποδεικνύει. ↩