Dayward AI
Week 1 · D3About 6 hours

Getting Started With the Pi SDK: the Three-Layer Architecture, Comparing It to the Agent Loop (dg P01/P02/M02/M03)

Rewrite day two's hand-written agent with the Pi SDK, compare it against the three-layer architecture and the Agent Loop in the source, and see what the framework does for you.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. State what each layer of the Pi SDK's three-layer architecture is responsible for
  2. Rewrite day two's tool-calling agent with the Pi SDK, with noticeably less code
  3. Compare the hand-written agent loop against the Pi SDK's built-in agent loop

After hand-writing that agent loop yesterday you probably came away with a feeling: this code has almost nothing to do with any particular business. Swap the tools, swap the task, and it still looks like that. So today we hand the code that is identical inside every agent over to a framework, and then reconcile it line by line: which of your lines went where, who does what on your behalf, and what you gave up in exchange. When you are done, come back to the top and tick off the three goals.

Plain-Language Walkthrough

Bare shell or move-in ready: what a framework is actually selling

There are two ways to take delivery of an apartment. Bare shell: wiring runs, plumbing, walls, and floors are all yours, three months of work, and you deal with seven or eight tradespeople along the way. The upside is that you watched every pipe go in, so when you later want to drill into a wall you know what is behind it. Move-in ready: sign the papers, carry your bags in, sleep there within a week. The price is that you never saw what is inside the walls, so moving one socket means first working out how the developer laid it out — and you may find you cannot move it at all.

That is exactly the relationship between a framework and hand-written code. Yesterday you built the bare shell: model calls, the loop, tool dispatch, feeding results back, the round guard — you laid every brick. Today you move into the finished apartment, but because you built one yourself once, you get to be the owner who understands what is behind the plaster.

So what is in the finished package? Pull up yesterday's code and it separates cleanly into two piles:

  • The part tied to your business: how the two tool manuals are worded, what execute looks up or computes, how the sentence shown to the user is assembled. Move to another project and all of it gets rewritten.
  • The part with nothing to do with your business: issuing the HTTP request, appending the reply to the history, reading the stop reason to decide whether to continue, mapping a tool name to a function, wrapping a result into a message and pushing it back, counting rounds to prevent a runaway. Move to another project and not one word of it changes.

The second pile is what the framework sells. It gathers that generic logic into a kernel and exposes only a handful of functions: register a tool, open a session, send a message, subscribe to events. Your code stops driving a machine and starts fitting parts into one.

State the engineering cost right now: the part you did not write did not disappear, it turned into defaults somebody else chose for you. Which model, what the system prompt says, how many rounds the loop may run, what happens when a tool errors — yesterday each of those was a visible line in your file, and today all of them live inside the kernel. They do not cease to exist because you cannot see them; they simply change value quietly during some framework upgrade, production behavior shifts with them, and your code never moved. That is the wiring behind the plaster, and it is why this chapter insists on a line-by-line reconciliation instead of copying an API out of the docs.

Pi's three layers: model, kernel, application

Pi is an Agent SDK written in TypeScript, and it cuts that kernel into three layers, each an independent npm package, with dependencies strictly one-directional downward:

TextText
@earendil-works/pi-coding-agent   application: sessions, resource loading, built-in tools, four run modes
        |  depends on
@earendil-works/pi-agent-core     kernel: the agent loop, tool execution, state management, the event stream
        |  depends on
@earendil-works/pi-ai             model: unified model calls, auth, model discovery, cost accounting

The bottom layer is the model-calling layer. It does exactly what your hand-written fetch did on D1, except it absorbs every vendor difference: request body shape, auth scheme, stream framing, the field names for tool calls all converge on one interface, and it tallies tokens and spend while it is at it. This layer also carries one hard constraint worth remembering — it only admits models that support tool calling, because a model that cannot call a tool is unusable in an agent setting.

The middle is the agent kernel. Built on the model layer, it owns precisely the loop you hand-wrote yesterday: take the model's reply, read the stop reason, run the tool, feed the result back, decide whether to go around again — and broadcast that whole process live as events. This is the layer this course actually wants you to read, because it is what your code from yesterday looks like once abstracted.

The top is the application layer. It addresses one concrete scenario, a coding agent in a terminal: how a session is stored and resumed, which directories extensions and skills load from, which tools ship built in, and four run modes — interactive, print, inter-process, and embedded SDK. Today we use only the last one, treating it as a library embedded in our own program.

Why cut it this way? Because each layer can be swapped out on its own and tested on its own. If all you want is a unified model-calling layer and you will write the loop yourself, install only the bottom. If you want the full agent loop but not the terminal interaction, stop at the middle. That is also a general test of whether a framework is any good: can its layering let you take half? Conversely, a framework you must swallow whole will eventually charge you for the half you never wanted.

The layering has one very practical benefit too: when something breaks, your first job is deciding which layer it broke in. A stack trace naming the model layer's package usually means auth, a model id, or a request-format problem; one naming the kernel means the loop or tool execution. The two investigations go in completely different directions, and that judgment comes from the three-layer picture you are memorizing right now in five minutes.

Line by line: where did every line you wrote yesterday go

First shrink yesterday's skeleton into its smallest form. It is the left half of the comparison table:

hand-rolled-loop.js
// D2's hand-written version: the loop, the dispatch, the refill, every line in your own file
const MAX_STEPS = 6 // same ceiling as the one in D2's text
 
async function runAgent(messages) {
  for (let step = 0; step < MAX_STEPS; step++) {
    const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}` },
      body: JSON.stringify({ model: 'openai/gpt-4o-mini', messages, tools: TOOL_SCHEMAS }),
    })
    const choice = (await res.json()).choices[0]
    messages.push(choice.message) // the history is yours to maintain
 
    // the anchor D2 left behind: the stop reason decides whether the loop continues
    const stopReason = choice.finish_reason
    if (stopReason !== 'tool_calls') return choice.message.content
 
    for (const call of choice.message.tool_calls) {
      const args = JSON.parse(call.function.arguments) // argument validation is yours too
      let result
      try {
        result = await TOOL_IMPLS[call.function.name](args) // manual dispatch
      } catch (err) {
        result = `Tool failed: ${err.message}` // you wrap the error into a message yourself
      }
      messages.push({ role: 'tool', tool_call_id: call.id, content: String(result) })
    }
  }
  throw new Error('Exceeded the maximum number of steps; it may be stuck in a loop')
}

Hand the same job to a session-based kernel and the calling side is down to four steps: register the tools, open a session, subscribe to events, send a message.

with-pi.js
// In a real project these four names are imported from '@earendil-works/pi-coding-agent';
// this course's lab reproduces the same API surface in a roughly 400-line pi-lite,
// which has the advantage of running offline with a readable kernel.
import { createAgentSession, ModelRuntime, SessionManager } from './pi-lite.js'
 
const modelRuntime = await ModelRuntime.create()
const model = (await modelRuntime.getAvailable())[0]
 
const { session } = await createAgentSession({
  model,
  modelRuntime,
  customTools: [getTimeTool, calcTool], // hand over the manual and the implementation only
  sessionManager: SessionManager.inMemory(), // the kernel maintains the history
})
 
session.subscribe((event) => {
  if (event.type === 'tool_execution_start') console.log(`calling tool: ${event.toolName}`)
})
 
// the loop, the dispatch, the refill, the stop check all happen inside the line below
await session.prompt('What time is it? Also work out 12 plus 30, then times 2')
session.dispose()

Now set the two side by side. This table is the most valuable thing here today:

The line you wrote yesterdayWhat it maps to in PiWhose job
fetch against the model endpoint, assembling the auth headerthe model layer's unified interface, with ModelRuntime picking an available modelframework
messages.push maintaining the history by handthe session holds the message list, SessionManager decides where it is storedframework
the for loop itselfthe kernel's agent loop; from outside you only see one await session.prompt()framework
reading the stop reason to decide whether to continuethe same check, moved into the kernel; the field is stopReason and the tool branch's value is toolUseframework
the JSON Schema inside the tools arraythe parameters passed to defineToolyou
TOOL_IMPLS[name] dispatching by handthe kernel finds the matching execute by tool nameframework
JSON.parse plus hand-written argument validationthe kernel validates against the schema and hands you structured argumentsframework
wrapping a result into a tool message and pushing it backexecute's return value, refilled by the kernelframework
try/catch returning an error stringthrow straight out of execute; the kernel turns it into an error-flagged tool result for the modelframework
the MAX_STEPS ceilingthe kernel's loop control and stop hooks, but what the ceiling should be is still your callshared
what a tool actually looks up or computesthe body of executeyou

One row in that table deserves a long stare: yesterday's stop-reason check did not disappear. Plenty of people assume that adopting a framework means "there is no loop any more." The loop is still there and so is the stop reason; they simply moved out of your if and into the kernel's if. Pi calls the field stopReason, with values covering a normal finish, hitting the length ceiling, wanting a tool, erroring, and being cancelled — and the tool branch's value is toolUse, which corresponds precisely to the line where you decided yesterday whether to go around again. The reason you cannot feel it from outside is that you switched observation methods: instead of reading a return value, you subscribe to events, which is the next section.

The agent loop: think → call tool → observe1/5
Think
Call tool
Observe
↺ Back to thinking, until the goal is met

The messages array (the whole thing gets resent every round)

userWhat's the temperature in Beijing today?
The user asks a question. So far, it's no different from an ordinary chat.

One number in passing: yesterday's agent ran about 120 lines, of which fewer than 30 were genuinely business logic. Moved onto a session-based kernel, the same functionality lands around 40 lines and those 30 business lines are untouched. The 80 lines you saved are exactly the cells marked "framework" in the table.

Framework or hand-written: one test you can actually apply

Saving 80 lines sounds like a sure thing, and engineering has no sure things. Here is a test you can apply: ask yourself whether you need to see and change every step inside this loop.

If you do, hand-write it. Three common cases. One, learning and debugging, where visible steps are the whole point — that is what yesterday was for. Two, behavior that must be controllable to the letter, such as a compliance requirement that every model call lands in an audit log and every tool call passes an approval gate, where the framework's hooks may not sit where you need them. Three, a genuinely tiny scenario: one or two tools, at most two rounds, where saving 40 lines does not cover the cost of pulling in a whole dependency tree (a complete agent framework often installs to hundreds of megabytes, so anything cold-start sensitive should price that first).

If you do not, use the framework. The test is whether you will eventually have to do all of these: more than five tools, pushing every agent step to a frontend live, sessions that survive a restart, automatic compression when the context fills, swapping models at will. None of those is hard alone; together they are a small framework — writing it yourself means reinventing one, and the version nobody else tests.

What is actually worth guarding against is not using a framework, it is using one without knowing what it does for you. Three concrete consequences.

First, the stack gets deeper when you debug. A tool that never ran might have a description the model could not parse, might have failed schema validation, or might have been stopped by one of the kernel's hooks. Somebody who knows the layering checks the event stream first for a tool-execution event: if it fired, the problem is in execution; if it never fired, the model never intended to call it.

Second, you inherit a pile of defaults you never wrote. It chose the model for you, it inserted the system prompt for you, it registered the built-in tools for you.

Third, an upgrade changes behavior you never tested. A default shifts, your code moves not one line, and production behaves differently — which is exceptionally hard to trace, because your first instinct will always be that you did not change anything.

One sentence to close: hand-write it once before adopting a framework, and you spend a day to save half a day on every future investigation. That is the entire reason this course puts D2 before D3.

Registering tools and subscribing to events: the two things you actually write

Once a framework is in play, your code is mostly down to two jobs: hand the tools in, take the events back.

Tools first. A tool has three parts: a manual written for the model (name, description, parameter schema), an implementation that does the work, and one registration. The most underrated part of the manual is description — it is not a comment for a colleague, it is the only basis on which the model decides whether and when to call this tool, and vagueness makes it skip a tool it should have used. D5 expands on exactly this.

tools.js
import { defineTool } from './pi-lite.js' // a real project imports from '@earendil-works/pi-coding-agent'
 
export const calcTool = defineTool({
  name: 'calc',
  label: 'Arithmetic', // display only, the model never sees it
  // description is the part written for the model: it decides whether and when to call
  description: 'Evaluate an arithmetic expression containing only digits, plus, minus, times, divide and parentheses, and return the result.',
  // TypeScript types are gone after compilation and unreadable at runtime, so the schema
  // has to restate the type; the three languages below keep type information at runtime
  // and can use the type declaration directly as the schema
  parameters: {
    type: 'object',
    properties: {
      expression: { type: 'string', description: 'The expression to evaluate, for example 12 plus 30 then times 2' },
    },
    required: ['expression'],
  },
  // params has already been validated against the schema above, so use it directly;
  // throw on failure and the kernel feeds the error back to the model
  async execute(_toolCallId, params) {
    return { content: [{ type: 'text', text: String(evaluate(params.expression)) }], details: {} }
  },
})

Notice the "throw on failure" line inside execute. It runs against most people's instinct: writing business code, we habitually swallow the exception and return an error object. Inside an agent, though, a tool's exception should be thrown at the kernel, which turns it into an error-flagged tool result for the model — and a model that sees something like "no such file" will often fix its argument and retry on the next turn. Swallow the exception and return "operation failed" as a normal result and the model concludes the tool succeeded. You implemented this rule by hand with try/catch yesterday; today it is a framework convention.

Now events. Yesterday, finding out what the agent was doing meant sticking a console.log in the loop. The loop is no longer yours, so observation comes from subscribing. One complete prompt roughly emits these:

TextText
agent_start              one prompt begins
- turn_start             one round begins (one model call plus that round's tool execution)
  - message_start        one message begins (assistant or toolResult)
  - message_update       a text delta, pushed per token, extremely frequent
  - message_end          one message ends
  - tool_execution_start a tool starts running
  - tool_execution_update a progress fragment while the tool runs
  - tool_execution_end   the tool finishes
- turn_end               one round ends, carrying that round's tool results
- agent_end              the loop is over; no further events follow

Lay this list over the comparison table from the previous section and a neat correspondence appears: turn_start and turn_end are one iteration of yesterday's for loop, and agent_end is the moment you hit return. The character-by-character text stream you got by hand-parsing SSE on D1 is here the text delta carried by message_update — the same thing, observed from a different seat.

Two traps in advance. First, message_update fires per token at very high frequency, so never do real work in that callback — writing to a database, issuing a request, or running a regular expression there drags the whole streaming path down, and the right move is to accumulate a small batch and process it together. Second, event subscription is your entry point for observability: latency stats, tool success rates, and cost attribution all tap off here, which is what D5 expands on.

Source Reading

Hands-On Lab

🧪 D3 lab: rewrite D2's agent with Pi

Code location: labs/agent-30days/day-03-pi-sdk-agent

Acceptance criteria:

  1. MOCK=1 pnpm start shows one complete multi-round tool interaction: two tool-call log lines first, then the final answer.
  2. Both tools get called, and execute receives structured arguments validated by the kernel against the schema rather than something you parsed yourself.
  3. Rerunning with --tool-error logs a tool error rather than a tool return, the model changes tack on the next round, and the program does not crash.
  4. The event log shows rounds beginning and ending in order, with a final line printing the stop reason and a total of 2 rounds.
  5. pnpm typecheck passes with no any.

Confirm one thing before you start: the kernel in this lab is a roughly 400-line pi-lite.ts, nearly a quarter of it comments, whose API shape corresponds one to one with Pi's public interface. That arrangement lets you run it offline and genuinely open the kernel to look at that loop — having the plaster taken off the wall once beats a hundred pages of documentation. The README gives the handful of edits that switch it to the real SDK.

  1. Run MOCK=1 pnpm start unmodified first: the model reaches only one tool and outright says it has no calculator to hand. Remember that incomplete output; it is your baseline for comparison.
  2. Write the tool manual properly: both description and the field descriptions are written for the model, and vagueness makes it hesitate. Rerun and watch the fake model go from unable to judge whether the tool applies to actually calling it.
  3. Register the second tool with the session, then rerun and confirm both tools get called.
  4. Add the round-event subscription, count how many rounds this conversation took, and compare it against the iteration count of yesterday's for loop.
  5. Delete the exception-swallowing try/catch inside execute, rerun with --tool-error to see how the model reacts on the next round, then open pi-lite.ts and find the implementation for every cell of the comparison table above.

Interview Questions

Today's four questions are in the bank below, weighted toward agent-loop design decisions and the framework-versus-hand-written trade-off. Expand a question and read the analysis before the key points — the follow-up on question 2, about the upgrade risk that framework defaults create, is the spot in this chapter interviewers press hardest, so do not skip it. Each is tagged for the China-domestic or overseas market so you can prioritize by where you are applying.

Checklist and Tomorrow

  • State what each layer of the Pi SDK's three-layer architecture is responsible for
  • Rewrite day two's tool-calling agent with the Pi SDK, with noticeably less code
  • Compare the hand-written agent loop against the Pi SDK's built-in agent loop
  • Point at every line of yesterday's hand-written loop and say who owns it inside the framework, especially the line checking the stop reason
  • All 5 acceptance criteria of the lab pass
  • Answer at least 3 of the 4 interview questions without looking at the key points

Tomorrow (D4) we defuse exactly those two time bombs. The first is the model: the framework picked one for you, so we pull the model-calling layer out, wire three providers into it ourselves, and make it fail over to the next one when a provider misbehaves. The second is the persona: that default system prompt you never wrote, which nonetheless decides how your agent talks, gets found, overridden with one you wrote, and verified as taking effect. Framework first and dismantling second is a deliberate order — you have to see which blanks the framework filled before you know which controls to take back.

Interview questions

  • What problems does an agent framework's built-in agent loop have to solve?Agent 框架内部的 Agent Loop 一般要解决哪些问题?
    Common in ChinaCommon overseasBasic#agent-loop#framework-design

    How to reason about it · think before answering

    1. This looks like a checklist question, but the real signal is whether you have written such a loop yourself. 'Call the model repeatedly until it stops' reads as documentation-only knowledge.
    2. The safest structure is to walk down your own hand-written loop line by line, because every line is one problem the kernel must own: issue the model request, maintain message history, decide from the stop reason whether to continue, dispatch by tool name, validate arguments against the schema, fold the tool result back in as a message, and cap the number of turns.
    3. Naming the stop reason explicitly scores well: the loop exits not when 'the model finished talking' but when the turn's stop reason is not a tool-use one. Most candidates blur past this, and it is the switch that drives the whole loop.
    4. Then add the three things a hand-rolled version usually skips but a framework cannot: running a batch of tool calls concurrently, emitting the whole run as an event stream so callers are not staring at a black box, and compaction plus session persistence once the context outgrows the window.
    5. Close on tool errors, which is where production experience shows: a failing tool should raise, and the kernel should turn that into a tool result flagged as an error so the model can fix its arguments and retry. Swallowing the exception and returning 'operation failed' as a normal result makes the model believe the tool succeeded.
    6. Expect the follow-up: how do you stop runaway loops? A max-turn cap is only a backstop; per-run token and wall-clock budgets plus a pre-execution hook that can block a call and hand the reason back to the model are what actually work.

    分析过程 · 先想清楚再作答

    1. 这题看着像背清单,区分度其实在「你有没有自己写过一遍」。只答「循环调用模型直到结束」会被认为读过文档但没写过代码。
    2. 最稳的拆法是把手写版的代码从上往下念一遍,每一行都是内核必须解决的一件事:发模型请求、维护消息历史、判断停止原因决定继不继续、按工具名分派、按 schema 校验参数、把工具结果回填成一条消息、控制最大轮数。这条链路念完,答案自然是完整的。
    3. 点名停止原因这一环最能加分:循环的出口条件不是「模型说完了」,而是这一轮的停止原因是不是「要调工具」。很多人把它含糊过去,而它恰恰是整个循环的开关。
    4. 然后补上手写版通常没做、但框架必须做的三件:并发执行同一批工具调用、把每一步以事件形式播报出去(否则外部完全是黑箱)、以及上下文超限时的压缩与会话持久化。
    5. 最后落到工具报错这一条,它是最能体现工程经验的:工具异常不应该被吞掉,要转成一条带错误标记的工具结果回给模型,让模型自己改参数重试;吞掉异常返回一句「操作失败」,模型会以为工具成功了。
    6. 可以预期的追问:怎么防死循环?答最大轮数只是兜底,更实际的是给单次运行设 token 与耗时预算,并在工具调用前留一个可以拦截的钩子,触发条件时把拦截原因回传给模型让它改道。

    Key points

    • The skeleton: call the model, maintain history, branch on the stop reason, dispatch tools, validate arguments, fold results back in
    • The stop reason is the loop's exit condition — a tool-use reason means one more turn, anything else means done
    • Tool execution details: batch calls can run concurrently, hooks belong before and after, and exceptions become error-flagged tool results the model can react to
    • An event stream is mandatory, otherwise callers see a black box and observability is impossible
    • Safety valves: max turns, token and latency budgets, context compaction, and session persistence for resume

    答题要点

    • 循环骨架:调模型、维护消息历史、按停止原因判断继不继续、分派工具、校验参数、回填工具结果
    • 停止原因是循环的出口条件,工具分支意味着还要再来一轮,其他取值意味着结束
    • 工具执行的工程细节:同一批调用可以并发、执行前后要留钩子、异常要转成带错误标记的工具结果回给模型
    • 对外要有事件流,否则调用方看不到 Agent 在做什么,也没法做可观测性
    • 安全阀:最大轮数、token 与耗时预算、上下文超限时的压缩,以及会话的持久化与恢复
  • How do you decide between adopting an agent framework and hand-rolling the loop?选择使用 Agent 框架还是手写 Agent,你会怎么权衡?
    Common in ChinaCommon overseasIntermediate#framework-design#engineering-tradeoffs

    How to reason about it · think before answering

    1. The hinge word is 'decide'. 'Frameworks are faster' and 'hand-rolling is more controllable' are each half an answer; what earns points is a criterion you can apply on the spot rather than a preference.
    2. Offer the criterion: ask whether you need to see and change every step inside the loop. If yes, hand-roll — during learning and debugging, under compliance rules that require every model call and tool call to be interceptable and auditable, or when the scenario really is two tools and three turns and the saved lines do not justify a large dependency tree.
    3. If no, take the framework, and justify it by what you will inevitably need anyway: more tools, streaming every step to a UI, sessions that survive a restart, compaction when context fills up, swapping models on demand. Assemble all of those yourself and you have written a small framework — an untested one.
    4. Then volunteer the three costs, which is where the signal is: debugging spans more layers, so a tool that never runs could be a bad description, a schema rejection, or a hook that blocked it; you inherit defaults you never wrote, including the model, the system prompt, and the built-in tools; and upgrades change behavior you never tested, which is brutal to diagnose because your own code did not change.
    5. Land on a practical middle: hand-roll once to internalize the loop, then adopt a framework, override its defaults explicitly, and pin its version. You keep the delivery speed without handing over control of behavior.
    6. Expect the follow-up: how do you judge a framework? By whether its layering lets you take only half of it — model layer only, loop your own. Anything you must swallow whole will eventually bill you for the half you do not use.

    分析过程 · 先想清楚再作答

    1. 题眼在「权衡」。答「框架更快」或者「手写更可控」都只说了一半,面试官想听的是你有没有一条能当场执行的判据,而不是立场。
    2. 给判据:问自己「我需不需要看见并改动这段循环里的每一步」。需要就手写——学习调试阶段、合规审计要求每次模型调用和工具调用都可拦截可留痕、或者场景本身只有一两个工具两三轮循环,那点代码量的收益抵不过一整棵依赖树。
    3. 不需要就用框架,判断标准是这几件事你是不是迟早都要做:工具数量上去、要把每一步实时推给前端、会话要能重启后继续、上下文满了要压缩、要随时换模型。这些凑齐了就是一个小型框架,自己写等于重新发明一个没人帮你测的版本。
    4. 然后主动说出框架的三笔代价,这是区分度所在:一是排障栈变深,工具没被调用可能是描述、schema、钩子拦截三种完全不同的原因;二是你继承了一堆没写过的默认值,模型、系统提示词、内置工具都是别人替你选的;三是升级会改变你没测过的行为,代码一行没动线上表现却变了,这类问题最难定位。
    5. 结论要给出可落地的折中:先手写一遍把循环吃透,再上框架;上了框架也要显式覆盖掉默认值,并把框架版本锁死。这样既拿到了开发速度,也没把行为的控制权整个交出去。
    6. 可以预期的追问:那你怎么评估一个框架好不好?答看它的分层能不能让你「只要一半」——只要模型调用层、循环自己写行不行;必须整包吞下的框架,迟早要为用不上的那一半付代价。

    Key points

    • The criterion is whether you need to see and modify every step of the loop
    • Hand-roll for learning and debugging, for compliance that demands interceptable and auditable steps, for genuinely tiny scenarios, and where dependency size or cold start matters
    • Use a framework once you need many tools, an event stream, persistent sessions, compaction, and model swapping — building all of that is writing a framework yourself
    • Three costs: deeper debugging surface, inherited defaults you never wrote, and upgrades that shift untested behavior
    • The middle path: hand-roll once, then adopt, override defaults explicitly, and pin the version

    答题要点

    • 判据是「需不需要看见并改动循环里的每一步」,需要就手写,不需要就用框架
    • 手写更合适:学习调试、合规要求每步可拦截可留痕、场景极简、对依赖体积与冷启动敏感
    • 框架更合适:工具多、要事件流、要会话持久化与压缩、要多模型——这些凑齐等于自己造一个框架
    • 框架的三笔代价:排障栈变深、继承一堆没写过的默认值、升级会改变没测过的行为
    • 折中做法:先手写吃透循环再上框架,显式覆盖默认值并锁死版本
  • What are the responsibilities of Pi SDK's three layers, and what does that layering buy you?Pi SDK 的三层架构分别对应什么职责?这样分层解决了什么问题?
    Common in ChinaCommon overseasBasic#framework-design#architecture

    How to reason about it · think before answering

    1. The first half is recall; the second half carries the signal. Reciting three package names without explaining the cut suggests you only skimmed the docs.
    2. State the layers precisely: the bottom is a unified model layer that normalizes each provider's request format, auth and streaming into one interface while tracking tokens and cost; the middle is the agent kernel built on top of it, owning the agent loop, tool execution, state and the event stream; the top is the application layer, owning session storage, extension and resource loading, built-in tools, and the interactive, print, RPC and embedded-SDK run modes. Dependencies point strictly downward.
    3. Then answer what it buys: layering lets you take only half. Want just a unified model layer and your own loop? Stop at the bottom. Want the full loop but none of the terminal UX? Stop in the middle. That test generalizes to any framework and is worth far more than the package names.
    4. Add the practical payoff: when something breaks, first place it in a layer. A stack trace through the model layer points at auth, a wrong model id or a malformed request; one through the kernel points at the loop or tool execution. The two investigations look nothing alike.
    5. Expect the follow-up: how does this map onto the loop you wrote by hand? All three layers were collapsed into one file — the fetch calls were the model layer, the while loop and tool dispatch were the kernel, and the CLI was the application layer. Making that mapping live is more convincing than any recitation.

    分析过程 · 先想清楚再作答

    1. 前半句是记忆题,后半句才有区分度。只背出三个包名而说不出「为什么这么切」,面试官会判断你只是照着文档看了一遍。
    2. 先把三层说准:最底层是统一的模型调用层,负责把各家 provider 的请求格式、鉴权、流式分包收敛成一套接口,还统计 token 与成本;中间是 Agent 内核层,构建在模型层之上,负责 Agent 循环、工具执行、状态管理和事件流;最上层是应用层,负责会话存取、扩展与资源装载、内置工具,以及交互式、打印、进程间调用、嵌入式 SDK 这几种运行模式。依赖方向严格单向向下。
    3. 然后回答「解决了什么」:分层的价值是让你能「只要一半」——只想要统一的模型调用层就停在最底层,想要完整循环但不要终端交互就停在中间层。这条判据可以用来评估任何框架,比复述包名有用得多。
    4. 补一个很实际的收益:排障时先判断问题落在哪一层。报错栈里出现模型层,多半是鉴权、模型 id 或请求格式;出现内核层,那是循环或工具执行;两者的排查方向完全不同。
    5. 可以预期的追问:这套分层跟你手写的版本怎么对应?答手写版把三层揉在了一个文件里——fetch 那几行是模型层,while 循环和工具分派是内核层,命令行交互是应用层。能当场做这个映射,比任何背诵都有说服力。

    Key points

    • Model layer: normalizes provider request formats, auth and streaming, tracks tokens and cost, and only ships tool-calling models
    • Kernel layer: the agent loop, tool execution and result folding, state management and the event stream, built on the model layer
    • Application layer: session storage, extension and resource loading, built-in tools, and the interactive, print, RPC and embedded-SDK run modes
    • Dependencies point one way, so each layer is replaceable and testable on its own and you can adopt only part of the stack
    • For debugging, place the failure in a layer first — model-layer and kernel-layer investigations diverge immediately

    答题要点

    • 模型层:统一各家 provider 的请求格式、鉴权与流式,附带 token 与成本统计,只收录支持工具调用的模型
    • 内核层:Agent 循环、工具执行与结果回填、状态管理、事件流,构建在模型层之上
    • 应用层:会话存取、扩展与资源装载、内置工具,以及交互式、打印、进程间调用、嵌入式 SDK 几种运行模式
    • 依赖单向向下,好处是每层可单独替换、单独测试,也能「只要一半」
    • 排障时先定位问题落在哪一层,模型层和内核层的排查方向完全不同
  • Once you adopt an agent framework, how do you know what it is doing internally, and where do you start debugging?用了 Agent 框架之后,你怎么知道它内部到底发生了什么?出问题从哪里查?
    Common in ChinaCommon overseasDeep dive#observability#framework-design#debugging

    How to reason about it · think before answering

    1. This is the hands-on version of the framework-versus-hand-rolling question, and it tests whether you have actually debugged on top of a framework. 'Add logging' is the weakest answer, because the loop is no longer in your code and there is nowhere to add it.
    2. Name the right observation point: the event stream. One run emits run start, each turn's start and end, message start and deltas and end, tool execution start and end, and run end. Those events are the loop's steps projected outward — turn start and end correspond to one iteration of your hand-written for loop, and run end to your return statement.
    3. Give a reusable triage chain, taking 'the tool never ran' as the example: check whether a tool-execution-start event was emitted. If it was, the problem lives in execution — arguments, implementation, timeout. If it was not, the model never decided to call it, so the problem is the tool description or the parameter schema and has nothing to do with the implementation. That single split removes most guesswork.
    4. Add two more threads: locate the failure by layer, since a model-layer stack points at auth, model id or request shape while a kernel-layer stack points at the loop or tool execution; and pin the framework version, because defaults shift between releases and 'behavior changed with no code change' almost always means an upgrade.
    5. Volunteer the production angle: the event stream is not just for debugging, it is the observability seam where per-step latency, tool success rate and token or cost accounting are collected. Warn that text-delta events fire per token, so heavy work in that callback stalls the stream — batch first, then process.
    6. Expect the follow-up: what if the framework does not expose the hook you need? Try dropping a layer first (bypass the application layer and drive the kernel directly), then its extension mechanism for intercepting around tool calls; forking is the last resort, and its real price is owning upstream merges forever.

    分析过程 · 先想清楚再作答

    1. 这题是「框架 vs 手写」那道题的实操版,考的是你有没有在框架上真的排过障。答「打日志」是最弱的答案,因为循环已经不在你的代码里了,你没有地方插日志。
    2. 先给正确的观察位置:框架的事件流。一次执行会依次发出运行开始、每一轮的开始与结束、消息的开始与增量与结束、工具执行的开始与结束、运行结束。这些事件就是循环的每一步在外部的投影——轮次的开始与结束对应手写版 for 循环的一次迭代,运行结束对应你 return 的那一刻。
    3. 给一条可复用的排查链:以「工具没被调用」为例,先看事件流里有没有发出工具执行开始的事件。发出了就是执行阶段的问题(参数、实现、超时);没发出就说明模型压根没决定调它,问题在工具描述或参数 schema,跟工具实现一点关系都没有。这条二分法能省掉大量瞎试。
    4. 补上另外两条线索:一是分层定位,报错栈落在模型层就查鉴权、模型 id 与请求格式,落在内核层就查循环与工具执行;二是把框架版本锁死,因为默认值随版本变化,「代码一行没改但行为变了」这类问题的第一嫌疑人就是升级。
    5. 生产视角要主动说:事件流不只是调试用的,它是可观测性的接入点——每一步耗时、工具成功率、token 与成本归集都从这里接出去。但要提醒一句,文本增量事件是逐 token 触发的,回调里做重活会拖慢整条流式链路,正确做法是攒一批再处理。
    6. 可以预期的追问:如果框架没有暴露你需要的那个钩子怎么办?答先看它的分层能不能降一层用(比如绕过应用层直接用内核层),再考虑用它的扩展机制在工具调用前后插手;实在不行才是 fork,而 fork 的代价是你从此要自己跟上游合并。

    Key points

    • Observe through the event stream, not ad-hoc logs: run start, turn start and end, message deltas, tool execution start and end, run end
    • Turn start and end map to one iteration of the hand-written loop, and run end maps to the return — that mapping makes any event table readable
    • Triage split: if a tool never ran, check for a tool-execution-start event; present means debug the implementation, absent means debug the description and schema
    • Locate by layer — model-layer stacks mean auth or model id, kernel-layer stacks mean the loop or tool execution — and pin the framework version, since upgrades silently move defaults
    • The event stream is also the observability seam, but text deltas fire per token, so batch before doing real work in that callback

    答题要点

    • 观察位置是框架的事件流,不是日志:运行开始、轮次开始与结束、消息增量、工具执行开始与结束、运行结束
    • 轮次的开始与结束对应手写版循环的一次迭代,运行结束对应 return,能做这个映射就能读懂任何事件表
    • 排查二分法:工具没被调用时,先看有没有发出工具执行开始的事件——发了查实现,没发查描述与 schema
    • 按分层定位:模型层的栈查鉴权与模型 id,内核层的栈查循环与工具执行;同时锁死框架版本,升级是行为变化的第一嫌疑人
    • 事件流也是可观测性接入点,但文本增量事件极其频繁,回调里不要做重活,攒一批再处理

Comments