IMEN891K / IDSC501 / AXGS-501 · Data Engineering · Fall 2026

17. Agents

Sangwoo Mo · POSTECH
Industrial & Management Engineering

Where We Are

Collection and PreparationPart I · Lec 2–5StoragePart II · Lec 6–13ML / LLM SystemsPart III · Lec 12–13RAGPart III · Lec 14AgentsPart III · Lec 15
Part III connects stored data to models, retrieval, and actions

Outline: Agents

01How an Agent Takes Actionschoose and run tools to complete atask02Controlling Workflows andStateguide the steps and resume afterfailures03Connecting Agents to Dataquery, change, and protect storedrecords

01 · How an Agent Takes Actions

01How an Agent Takes Actionschoose and run tools to complete atask02Controlling Workflows andStateguide the steps and resume afterfailures03Connecting Agents to Dataquery, change, and protect storedrecords

§ Reasoning and Actions

How an Agent Takes ActionsReasoning and ActionsChoose the next actionReActTool callingTool ExecutionExecute and bound actionsTool contract / MCPBounded loop

Recap: Retrieval and ToolsA retrieval pipeline fixes the search step before generation

Recap: Retrieval and Tools
Agents add tool use to the components seen so far

AgentAn agent uses a model to choose actions toward a task

stateObserveDecideActUpdateToolsread · query · writeStopfinal answer or escalate
The loop repeats until a stop rule fires

ReActReAct alternates reasoning, actions, and observations

ReAct
An observation can change the next action

ReAct ExampleTool observations provide facts that the model can use

ReAct Example
The answer depends on observations from external tools

Caveat: Unsupported ActionA model can request a tool that does not match the task

Taskfind the latest pressure readingModel proposesdelete_old_readings()Runtime checkallowed for this task:read_pressure onlyrejectedErrorto model
The runtime, not the model, decides what may run

Tool CallingThe model proposes a structured tool name and arguments

ApplicationModelToolquestion + tool schemascall read_pressure{"pump_id": "P-17"}execute{"pressure_bar": 4.2}tool result messagefinal answer
The model asks; the application executes

Tool DefinitionA typed LangChain tool executes one bounded read

const readPressure = tool(({pump_id}) => {
  const row = readings[pump_id];
  if (!row) throw Error("Unknown pump");
  return {pump_id, ...row};
}, {name:"read_pressure", description:"Read one pump pressure in bar",
  schema:z.object({pump_id:z.enum(["P-17","P-18"])})});
return await readPressure.invoke({pump_id:"P-17"});

§ Tool Execution

How an Agent Takes ActionsReasoning and ActionsChoose the next actionReActTool callingTool ExecutionExecute and bound actionsTool contract / MCPBounded loop

Tool ContractSpecify what a tool accepts, changes, and returns

read_pressureone bounded readInput schemapump_id ∈ {P-17, P-18}Side effectsnone: read onlyOutput schemapump_id, pressure_bar, read_atError classesUnknownPump · Timeout (retry)
Four facts every tool must declare

Model Context ProtocolMCP standardizes how applications connect to capability servers

Host: agent applicationMCP client 1MCP client 2protocolprotocolFiles servertools · resourcesDatabase servertools · resourcesManual folderFactory SQL DB
One protocol between the host and every capability server

Caveat: MCP and PermissionA connected server may expose more capability than a task needs

Server advertisesread_readingswrite_readinghiddendelete_readingshiddenThis runtask needs reading onlyModel seesread_readings
Expose only the tools this task needs

Agent RuntimeThe runtime owns execution, validation, and state updates

Model outputproposed actionRuntimeValidatecontract · accessExecutetool codeRecordobservationModelnext decisioninvalid → error observation, not execution
Invalid actions return as errors, not executions

Bounded Agent LoopObserve a model decision, tool execution, and the next decision

const model = makeModel([
  '{"tool":"read_pressure","pump_id":"P-17"}',
  '{"final":"P-17 pressure is 4.2 bar; threshold 5 bar."}'
]);
const messages = [new HumanMessage("Check P-17 pressure. Return JSON tool or final.")];
const trace = [];
for (let step=0; step<3; step++) {
  const raw = await model.invoke(messages);
  const decision = await new JsonOutputParser().invoke(raw);
  trace.push({step,decision});
  if (decision.final) return {mode:MODEL_MODE, trace};
  if (decision.tool !== "read_pressure") throw Error("Unknown tool");
  const observation = await pressureTool.invoke({pump_id:decision.pump_id});
  trace.push({observation});
  messages.push(raw, new HumanMessage(JSON.stringify(observation)));
}
throw Error("Step budget exceeded");

Stopping RulesStop on completion, limits, or an unresolved failure

agent loopstep 3 of max 8evidence sufficientvalidate and returnstep / token / time limitpartial status, escalatesame failure repeatedstop the loopneeds authorizationrequest approval
Four ways a loop ends; only one is success

Demo: Bounded Agent LoopInspect the decision and observation sequence

  1. Request P-18 instead of P-17. Compare the observation with the recorded final response.
  2. Replace the final response with another tool request. Observe the step limit.
Run code to inspect its output.
Ready
LangChain.js · JavaScript · Ctrl/Cmd+Enter · Offline replay uses fixed responses; local mode calls your model

Answer: Bounded Agent LoopThe baseline output comes from executing the shown code

{"mode": "Recorded response (no inference)", "trace": [{"step": 0, "decision": {"tool": "read_pressure", "pump_id": "P-17"}}, {"observation": {"pump_id": "P-17", "pressure_bar": 4.2, "age_minutes": 2}}, {"step": 1, "decision": {"final": "P-17 pressure is 4.2 bar; threshold 5 bar."}}]}

02 · Controlling Workflows and State

01How an Agent Takes Actionschoose and run tools to complete atask02Controlling Workflows andStateguide the steps and resume afterfailures03Connecting Agents to Dataquery, change, and protect storedrecords

§ Control Flow

Controlling Workflows and StateControl FlowChoose the execution pathStateGraphConditional edgesState and RecoveryResume a run safelyMemoryCheckpoints / approval

Chain CompositionA chain fixes the order of prompt, model, and parsing stages

Chain Composition
Fixed composition is suitable when the dependencies are known

Adaptive Control FlowAn agent may choose a different next step after observing a result

Adaptive Control Flow
The observation changes which branch runs

Kinds of AgentsAgents differ in how much of the control flow the model decides

Kinds of Agents
A router picks one branch; an autonomous agent picks every step

Control and ReliabilityGiving the model more control tends to lower application reliability

Control and Reliability
A developer-defined graph keeps model choices inside fixed structure

Controlled WorkflowDevelopers define parts of the control flow explicitly

Controlled Workflow
Some steps follow a fixed path

LangGraphLangGraph represents stateful workflows with nodes and edges

LangGraph
Nodes update state; edges select the next node

Workflow GraphUse conditional edges to send stale readings to review

const State = Annotation.Root({pump_id:Annotation(), reading:Annotation(),
  outcome:Annotation()});
const graph = new StateGraph(State)
 .addNode("read", async s => ({reading:await pressureTool.invoke({pump_id:s.pump_id})}))
 .addNode("report", s => ({outcome:s.reading.pressure_bar > 5 ? "alarm" : "normal"}))
 .addNode("review", () => ({outcome:"stale; review required"}))
 .addEdge(START,"read")
 .addConditionalEdges("read", s => s.reading.age_minutes > 10 ? "review" : "report")
 .addEdge("report",END).addEdge("review",END)
 .compile();
const result = await graph.invoke({pump_id:"P-17"});
return {reading:result.reading, outcome:result.outcome};

Demo: Stateful Inspection GraphChange the pump and inspect the selected state transition

  1. Run P-18. Which branch handles the stale reading?
  2. Change the freshness limit from 10 to 60. Does the outcome change?
Run code to inspect its output.
Ready
LangChain.js · JavaScript · Ctrl/Cmd+Enter · Real library execution with local data

Answer: Stateful Inspection GraphThe baseline output comes from executing the shown code

{
  "reading": {
    "pump_id": "P-17",
    "pressure_bar": 4.2,
    "age_minutes": 2
  },
  "outcome": "normal"
}

§ State and Recovery

Controlling Workflows and StateControl FlowChoose the execution pathStateGraphConditional edgesState and RecoveryResume a run safelyMemoryCheckpoints / approval

Agent StateState contains the information needed to continue the current run

Run statemessagesrequest, tool observationstaskpump_id P-17, window 24 h, pending actionprogressread done; compare pendingcontrolbudget 5 of 8, retries 1, approval nonemodel contextfor this callthe runtime keeps the rest
The model sees a slice of the run state

Memory TypesDifferent retained information supports different decisions

TypeContainsExample
Working stateCurrent task progressA pending tool call
Semantic memoryStored factsPreferred report format
Episodic memoryPast events and outcomesA resolved maintenance case
Procedural memoryReusable instructionsA validated inspection procedure
Choose what to remember before choosing where to store it

Caveat: Stale MemoryA stored fact can outlive the condition that made it true

MonTueWedMemory written"P-17 under maintenance"Maintenance completeddatabase status: activeAgent retrieves memorystill says "under maintenance"Store time and source with the fact; recheck the system of record
A remembered fact outlived the condition

CheckpointA checkpoint saves the state needed to resume a workflow

readanalyzeapprovewritecrash during approveresume from the last saved statecheckpoint saved
A checkpoint after each node limits repeated work

Caveat: Resume and Side EffectsResuming a workflow can repeat an external write

checkpointcreate_ticket → #9crashresumecreate_ticket → #10restarts after the last checkpointWith request_id req_42, the second call returns #9
Resuming repeated a write

Approval BoundaryPause before a consequential action and resume with a recorded decision

proposepauseoperator reviews the previewapprove → executedecision recorded with the runreject → stopnothing written
The run waits for a recorded decision

Agent TraceTrace actions and observations instead of relying on the final narrative

Agent Trace
The trace shows each node, state update and message

03 · Connecting Agents to Data

01How an Agent Takes Actionschoose and run tools to complete atask02Controlling Workflows andStateguide the steps and resume afterfailures03Connecting Agents to Dataquery, change, and protect storedrecords

§ Text-to-SQL

Connecting Agents to DataText-to-SQLTranslate an analytical questionSchema / metricsRead-only query toolWrites and AccessChange and protect recordsTransactions / retriesRoles / masking

Data AgentsHere, a data agent is an agent that reads and writes data systems

Agenttask · statesql_queryreadFactory SQL DBdoc_searchreadManual indexcreate_requestwriteRepairs table
Reads and writes go through separate tools

Text-to-SQLText-to-SQL translates a natural-language question into a database query

QuestionWhich line has the higher yield?Schema contexttables, keys, yield definitionModelSELECT m.line, SUM(r.good) * 1.0 / SUM(r.total)FROM Runs r JOIN Machines m ON m.id = r.machine_idGROUP BY m.lineline yieldA 83.75%B 95.00%
Schema context turns a question into a checkable query

Recap: Factory TablesMachine 1 is P-17; each row in Runs records one production run

Machinesid (PK)lineone row per machineRunsid (PK)machine_id (FK)daygood, totalone row per runRepairsrequest_id (UNIQUE)machine_id (FK)reason, statusone row per request1*1*
Keys and grain decide which joins are valid

Schema ContextExpose names, types, keys, and grain before generating SQL

Catalogtables and columnsKeysRuns.machine_id → Machines.idGrainRuns: one row per runMetricyield = Σ good / Σ totalPromptschema context+ questionSQLjoins on the keysums before dividing
The prompt carries keys, grain and the metric

Weighted YieldAggregate counts before dividing when run sizes differ

run 180 good of 100run 29 of 10Σ good / Σ total = 89 / 11080.9%: weights runs by sizemean of ratios = (80% + 90%) / 285%: the 10-unit run counts as much
Sum the counts, then divide

Line Yield QueryJoin by the declared key and divide the aggregated counts

SELECT m.line, SUM(r.good) AS good, SUM(r.total) AS total,
       ROUND(100.0 * SUM(r.good) / NULLIF(SUM(r.total),0),2) AS yield_pct
FROM Runs r JOIN Machines m ON m.id=r.machine_id
GROUP BY m.line ORDER BY m.line;
The result uses production counts rather than an average of run ratios

Caveat: Join MultiplicationAn extra one-to-many join can inflate both counts and rankings

Runs: run 1good 80 · total 100note 1 for run 1run 1 · 80 · 100note 2 for run 1run 1 · 80 · 100note 3 for run 1run 1 · 80 · 100SUM(total)300, not 100Aggregate notes per run first, then join
Three notes triple the production count

Query Validation LayersCheck syntax, access, resources, and semantics separately

runSyntaxdialect parserSELEC line FROM RunsAccessallowed tablesSELECT * FROM PayrollResourcestimeout · scan capfull scan, 2 billion rowsSemanticsmetric · keys · grainAVG(good * 1.0 / total)
A query that runs can still be wrong

Read-Only Query ToolExecute queries under a constrained database identity

Validated SQLquery candidateDB roleauthorized read scopeBudgettimeout / scan capResultrows / metadata
Validated SQL → DB role → Budget → Result

Demo: Factory YieldRun the calculation in the local SQLite database

  1. Return total good and total output for each line
  2. Calculate weighted yield as a percentage
Ctrl/⌘ + Enter

Answer: Factory YieldLine A yields 83.75%; line B yields 95%

SELECT m.line, SUM(r.good) AS good, SUM(r.total) AS total,
       ROUND(100.0*SUM(r.good)/NULLIF(SUM(r.total),0),2) AS yield_pct
FROM Runs r JOIN Machines m ON m.id=r.machine_id
GROUP BY m.line ORDER BY m.line;
The answer follows the stated metric and joins

Text-to-SQL ExecutionPass schema and metric meaning into the model, then execute the query

const prompt = ChatPromptTemplate.fromMessages([
 ["system", "Return JSON with sql. Only read Machines and Runs. " + DB_SCHEMA],
 ["human", "{question}"]
]);
const model = makeModel([JSON.stringify({sql:YIELD_SQL})]);
const proposed = await prompt.pipe(model).pipe(new JsonOutputParser())
 .invoke({question:"Yield by line over all runs?"});
const rows = await dbQuery(proposed.sql); // SQLite enforces query-only mode
return {mode:MODEL_MODE, sql:proposed.sql, rows};

Demo: Text-to-SQL ExecutionInspect the proposed SQL and the rows returned by SQLite

  1. Replace SUM(good)/SUM(total) with AVG(1.0*good/total). Compare line A.
  2. Change the proposed query to DELETE FROM Runs. Inspect query-only enforcement.
Run code to inspect its output.
Ready
LangChain.js · JavaScript · Ctrl/Cmd+Enter · Offline replay uses fixed responses; local mode calls your model

Answer: Text-to-SQL ExecutionThe baseline output comes from executing the shown code

{"mode": "Recorded response (no inference)", "sql": "SELECT m.line, ROUND(100.0*SUM(r.good)/SUM(r.total),2) yield_pct FROM Runs r JOIN Machines m ON m.id=r.machine_id GROUP BY m.line ORDER BY m.line", "rows": [{"line": "A", "yield_pct": 83.75}, {"line": "B", "yield_pct": 95}]}

§ Writes and Access

Connecting Agents to DataText-to-SQLTranslate an analytical questionSchema / metricsRead-only query toolWrites and AccessChange and protect recordsTransactions / retriesRoles / masking

Case study: Repair Request WorkflowA repair task crosses query, permission, and write boundaries

StageContractEvidence
Find machineAuthorized lookupMachine ID + current state
Check readingsFreshness + unitsTimestamped measurements
Propose repairBounded write + reviewTarget + reason + request ID
Create requestTransaction + idempotencyStored row + receipt
The final answer follows verified reads and a confirmed write

Transaction BoundaryA transaction groups changes that must succeed or fail together

one transactionINSERT INTO Repairs (req_42, ...)UPDATE Machines SET status = 'hold'COMMITboth changes are storedROLLBACKneither change is storedNever: a request without the status change
Both writes happen, or neither

Idempotent RequestA uniqueness constraint can prevent duplicate requests

CREATE TABLE Repairs (
  request_id TEXT UNIQUE,
  machine_id INTEGER,
  reason TEXT,
  status TEXT
);
-- The server also verifies that repeated IDs carry the same payload.

Caveat: Timeout After CommitA missing response does not reveal whether the write succeeded

AgentTool serverDatabasecreate_request(req_42)INSERT + COMMITokresponse lost (timeout)retry create_request(req_42)SELECT ... WHERE request_id = req_42existing request returned
The retry finds the committed request

Demo: Duplicate Repair RequestUse SQLite to observe a unique request key

  1. Insert req_42 twice in one execution using INSERT OR IGNORE
  2. Select Repairs and verify that one row remains
Ctrl/⌘ + Enter

Answer: Duplicate Repair RequestThe unique key leaves one record after two identical inserts

INSERT OR IGNORE INTO Repairs VALUES ('req_42',1,'vibration','open');
INSERT OR IGNORE INTO Repairs VALUES ('req_42',1,'vibration','open');
SELECT * FROM Repairs;
This example demonstrates uniqueness, not the entire write policy

Role-Based Access ControlRBAC grants capabilities to roles assigned to authenticated identities

Identityverified callerRoleanalyst / operatorPermissionread / writeResourceapproved assets
Identity → Role → Permission → Resource

Caveat: Prompt-Based AccessA prompt cannot replace a database permission check

Prompt: "do not show salaries"advice to the modelGenerated SQLSELECT salary FROM PayrollDB role agent_readercan read PayrollSalaries returnedFix in the databaserevoke Payroll from agent_reader → permission denied
Only the database permission stops the query

Row Filters and Column MasksFilter records and redact sensitive values at query time

Contacts (source)machine line email1 A op1@example.test2 A op2@example.test3 B op3@example.testOperator, line Amachine line email1 A op1@example.test2 A op2@example.testfiltered outAnalystmachine line email1 A [masked]2 A [masked]3 B [masked]Row filter by line; column mask by role; the source row is unchanged
Same table, different visibility per caller

Column Masking ExampleThe same row can expose different values to different roles

machine_idRoleemail visible
1Operator with approved accessoperator1@example.test
1Analyst[masked]
Masking changes visibility without changing the source record

Agent EvaluationTest the outcome, the action path, and the database state

DimensionCheck
Task successRequired result verified by evidence
Tool behaviorCorrect tool, arguments, and access scope
RecoveryTimeouts and duplicate requests handled
EfficiencyCalls, tokens, and elapsed time
An agent test observes both the final answer and the changed state

Takeaway: Bound actions, recover state, govern data

1Validate tool requests and bound the agent loop
2Keep workflow state outside the model so runs can pause and resume
3Connect agents to data through read-only queries, transactions, and roles
← → / Space · S notes · O overview · F fullscreen · number + Enter
Speaker notes