pi-python-core 0.10.1__py3-none-any.whl → 0.10.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pi_python/__init__.py +2 -0
- pi_python/_version.py +1 -1
- pi_python/agent.py +53 -4
- pi_python/events.py +3 -4
- pi_python/features.py +1 -0
- pi_python/loop.py +36 -6
- pi_python/messages.py +57 -0
- pi_python/run.py +91 -11
- pi_python/tools.py +39 -2
- {pi_python_core-0.10.1.dist-info → pi_python_core-0.10.2.dist-info}/METADATA +1 -1
- {pi_python_core-0.10.1.dist-info → pi_python_core-0.10.2.dist-info}/RECORD +14 -14
- {pi_python_core-0.10.1.dist-info → pi_python_core-0.10.2.dist-info}/WHEEL +0 -0
- {pi_python_core-0.10.1.dist-info → pi_python_core-0.10.2.dist-info}/licenses/LICENSE +0 -0
- {pi_python_core-0.10.1.dist-info → pi_python_core-0.10.2.dist-info}/licenses/NOTICE +0 -0
pi_python/__init__.py
CHANGED
|
@@ -64,6 +64,7 @@ from .recovery import (
|
|
|
64
64
|
)
|
|
65
65
|
from .tools import (
|
|
66
66
|
Tool,
|
|
67
|
+
ToolInvocation,
|
|
67
68
|
ToolContext,
|
|
68
69
|
ToolExecutor,
|
|
69
70
|
ToolResult,
|
|
@@ -151,6 +152,7 @@ __all__ = [
|
|
|
151
152
|
"is_recoverable_length",
|
|
152
153
|
"is_retryable_error",
|
|
153
154
|
"retry_delay",
|
|
155
|
+
"ToolInvocation",
|
|
154
156
|
"ToolContext",
|
|
155
157
|
"ToolExecutor",
|
|
156
158
|
"ToolResult",
|
pi_python/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.10.
|
|
1
|
+
__version__ = "0.10.2"
|
pi_python/agent.py
CHANGED
|
@@ -9,6 +9,7 @@ import asyncio
|
|
|
9
9
|
from copy import copy, deepcopy
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from typing import Any, Callable
|
|
12
|
+
from uuid import uuid4
|
|
12
13
|
|
|
13
14
|
from .cancellation import CancelToken
|
|
14
15
|
from .errors import (
|
|
@@ -29,6 +30,7 @@ from .messages import (
|
|
|
29
30
|
TextContent,
|
|
30
31
|
Message,
|
|
31
32
|
SystemMessage,
|
|
33
|
+
ToolCall,
|
|
32
34
|
UserMessage,
|
|
33
35
|
validate_history,
|
|
34
36
|
validate_json,
|
|
@@ -37,7 +39,7 @@ from .provider import Provider, DefaultProvider, FunctionProvider, has_default_s
|
|
|
37
39
|
from .queues import MessageQueues
|
|
38
40
|
from .run import CALLER_CANCELLED, Run, RunResult, merge
|
|
39
41
|
from .sync import run_sync
|
|
40
|
-
from .tools import Tool
|
|
42
|
+
from .tools import Tool, ToolInvocation
|
|
41
43
|
from .transcript import current_system_message
|
|
42
44
|
|
|
43
45
|
|
|
@@ -78,9 +80,13 @@ class Agent:
|
|
|
78
80
|
execution_mode: str = "parallel",
|
|
79
81
|
steering_mode: str = "one_at_a_time",
|
|
80
82
|
follow_up_mode: str = "one_at_a_time",
|
|
83
|
+
include_tool_origin: bool = False,
|
|
81
84
|
):
|
|
82
85
|
if execution_mode not in {"parallel", "sequential"}:
|
|
83
86
|
raise ConfigurationError("Invalid execution mode")
|
|
87
|
+
if type(include_tool_origin) is not bool:
|
|
88
|
+
raise ConfigurationError("include_tool_origin must be a boolean")
|
|
89
|
+
self.include_tool_origin = include_tool_origin
|
|
84
90
|
self._queues = MessageQueues(steering_mode, follow_up_mode)
|
|
85
91
|
if provider is not None and stream_fn is not None:
|
|
86
92
|
raise ConfigurationError("Pass provider or stream_fn, not both")
|
|
@@ -204,6 +210,10 @@ class Agent:
|
|
|
204
210
|
if self._running:
|
|
205
211
|
self._updates.append(update)
|
|
206
212
|
else:
|
|
213
|
+
# Preserve submission order after an application batch deferred updates.
|
|
214
|
+
for pending in self._updates:
|
|
215
|
+
merge(self._defaults, pending)
|
|
216
|
+
self._updates.clear()
|
|
207
217
|
merge(self._defaults, update)
|
|
208
218
|
|
|
209
219
|
@staticmethod
|
|
@@ -332,6 +342,31 @@ class Agent:
|
|
|
332
342
|
raise AgentBusyError("Agent already running")
|
|
333
343
|
return await self._start(self._input(message))
|
|
334
344
|
|
|
345
|
+
async def execute_tools(
|
|
346
|
+
self, invocations: list[ToolInvocation], *, source: str | None = None
|
|
347
|
+
) -> RunResult:
|
|
348
|
+
"""Execute a nonempty batch without a model request or consuming queued input.
|
|
349
|
+
|
|
350
|
+
Shares the model tool path, limits and cancellation ownership. Results are
|
|
351
|
+
recorded as application feedback, never as assistant/tool-result pairs.
|
|
352
|
+
``source`` is caller-supplied diagnostic metadata, not an identity claim.
|
|
353
|
+
"""
|
|
354
|
+
self._usable()
|
|
355
|
+
if self._running:
|
|
356
|
+
raise AgentBusyError("Agent already running")
|
|
357
|
+
validate_history(self._messages)
|
|
358
|
+
if not isinstance(invocations, list) or not invocations:
|
|
359
|
+
raise ConfigurationError("execute_tools requires a nonempty list of ToolInvocation")
|
|
360
|
+
if source is not None and not isinstance(source, str):
|
|
361
|
+
raise ConfigurationError("source must be a string or None")
|
|
362
|
+
calls = []
|
|
363
|
+
for invocation in invocations:
|
|
364
|
+
if not isinstance(invocation, ToolInvocation):
|
|
365
|
+
raise ConfigurationError("Expected ToolInvocation")
|
|
366
|
+
invocation.__post_init__()
|
|
367
|
+
calls.append(ToolCall(str(uuid4()), invocation.name, deepcopy(invocation.arguments)))
|
|
368
|
+
return await self._start([], application_calls=calls, source=source)
|
|
369
|
+
|
|
335
370
|
def prompt_sync(
|
|
336
371
|
self, message: str | Message | list[Message], images: list[ImageContent] | None = None
|
|
337
372
|
) -> RunResult:
|
|
@@ -369,18 +404,32 @@ class Agent:
|
|
|
369
404
|
return await self._start([])
|
|
370
405
|
|
|
371
406
|
async def _start(
|
|
372
|
-
self,
|
|
407
|
+
self,
|
|
408
|
+
pending: list[Message],
|
|
409
|
+
skip_initial_steering: bool = False,
|
|
410
|
+
*,
|
|
411
|
+
application_calls: list[ToolCall] | None = None,
|
|
412
|
+
source: str | None = None,
|
|
373
413
|
) -> RunResult:
|
|
374
|
-
if
|
|
414
|
+
if (
|
|
415
|
+
application_calls is None
|
|
416
|
+
and isinstance(self.provider, DefaultProvider)
|
|
417
|
+
and not has_default_stream()
|
|
418
|
+
):
|
|
375
419
|
raise ConfigurationError(
|
|
376
420
|
"No model provider: pass Agent(provider=...) or call set_default_stream_fn(...)"
|
|
377
421
|
)
|
|
422
|
+
for update in self._updates:
|
|
423
|
+
merge(self._defaults, update)
|
|
424
|
+
self._updates.clear()
|
|
378
425
|
self._running = True
|
|
379
426
|
self._idle.clear()
|
|
380
427
|
self._settled.clear()
|
|
381
428
|
self._last_error = None
|
|
382
429
|
self._events.failed = False
|
|
383
|
-
run = self._run = Run(
|
|
430
|
+
run = self._run = Run(
|
|
431
|
+
self, skip_initial_steering, application_calls=application_calls, source=source
|
|
432
|
+
)
|
|
384
433
|
run.loop = asyncio.get_running_loop()
|
|
385
434
|
# Driver starts with a checkpoint so cancellation before scheduling still finalizes.
|
|
386
435
|
run.driver = asyncio.create_task(self._drive(run, pending))
|
pi_python/events.py
CHANGED
|
@@ -16,7 +16,7 @@ from .messages import validate_json
|
|
|
16
16
|
class Event:
|
|
17
17
|
type: str
|
|
18
18
|
run_id: str
|
|
19
|
-
turn_id: int
|
|
19
|
+
turn_id: int | None
|
|
20
20
|
sequence: int
|
|
21
21
|
call_id: str | None = None
|
|
22
22
|
data: dict[str, Any] = field(default_factory=dict)
|
|
@@ -50,8 +50,7 @@ def decode_event(value: str) -> Event:
|
|
|
50
50
|
if (
|
|
51
51
|
type(e.sequence) is not int
|
|
52
52
|
or e.sequence < 1
|
|
53
|
-
or type(e.turn_id) is not int
|
|
54
|
-
or e.turn_id < 0
|
|
53
|
+
or (e.turn_id is not None and (type(e.turn_id) is not int or e.turn_id < 0))
|
|
55
54
|
):
|
|
56
55
|
raise MessageValidationError("Invalid event sequence or turn")
|
|
57
56
|
if e.call_id is not None and not isinstance(e.call_id, str):
|
|
@@ -85,7 +84,7 @@ class EventDispatcher:
|
|
|
85
84
|
self,
|
|
86
85
|
kind: str,
|
|
87
86
|
run_id: str,
|
|
88
|
-
turn_id: int,
|
|
87
|
+
turn_id: int | None,
|
|
89
88
|
data: dict[str, Any],
|
|
90
89
|
call_id: str | None,
|
|
91
90
|
await_owned: Callable,
|
pi_python/features.py
CHANGED
pi_python/loop.py
CHANGED
|
@@ -19,6 +19,7 @@ from .messages import (
|
|
|
19
19
|
message_from_dict,
|
|
20
20
|
message_to_dict,
|
|
21
21
|
validate_history,
|
|
22
|
+
convert_application_tool_batch,
|
|
22
23
|
)
|
|
23
24
|
from .models import ModelInfo
|
|
24
25
|
from .provider import ModelRequest
|
|
@@ -82,6 +83,12 @@ async def stream_response(run: Run) -> tuple[AssistantMessage, bool]:
|
|
|
82
83
|
messages = await run.hook("transform_context", messages, run.token)
|
|
83
84
|
if run.hooks.convert_to_llm:
|
|
84
85
|
messages = await run.hook("convert_to_llm", deepcopy(messages))
|
|
86
|
+
messages = [
|
|
87
|
+
convert_application_tool_batch(m)
|
|
88
|
+
if isinstance(m, CustomMessage) and m.custom_type == "application_tool_batch"
|
|
89
|
+
else m
|
|
90
|
+
for m in messages
|
|
91
|
+
]
|
|
85
92
|
if any(isinstance(m, CustomMessage) for m in messages):
|
|
86
93
|
raise UnsupportedCapabilityError("Custom messages require convert_to_llm")
|
|
87
94
|
validate_history(messages)
|
|
@@ -212,6 +219,8 @@ async def execute_batch(run: Run, outcomes: list[ToolOutcome]) -> None:
|
|
|
212
219
|
agent_context=snapshot["context"],
|
|
213
220
|
_on_reconciliation_required=lambda: run.require_reconciliation(),
|
|
214
221
|
)
|
|
222
|
+
if run.application_batch:
|
|
223
|
+
contexts[id(outcome)]._set_origin(outcome.origin, outcome.source)
|
|
215
224
|
return contexts[id(outcome)]
|
|
216
225
|
|
|
217
226
|
def abort_before_start(outcome: ToolOutcome) -> bool:
|
|
@@ -300,6 +309,32 @@ async def execute_batch(run: Run, outcomes: list[ToolOutcome]) -> None:
|
|
|
300
309
|
await run.commit_outcome(outcome)
|
|
301
310
|
|
|
302
311
|
|
|
312
|
+
def exceeds_tool_budget(run: Run, outcomes: list[ToolOutcome], used: int = 0) -> bool:
|
|
313
|
+
maximum = run.limits.max_tool_calls
|
|
314
|
+
if maximum is None or used + len(outcomes) <= maximum:
|
|
315
|
+
return False
|
|
316
|
+
for outcome in outcomes:
|
|
317
|
+
outcome.result = error_result("limit", "Entire tool batch exceeds remaining budget")
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
async def run_application_batch(run: Run) -> tuple[str, str]:
|
|
322
|
+
run.checkpoint()
|
|
323
|
+
await run.emit("agent_start")
|
|
324
|
+
if exceeds_tool_budget(run, run.active_batch):
|
|
325
|
+
return "limit_reached", "max_tool_calls"
|
|
326
|
+
await execute_batch(run, run.active_batch)
|
|
327
|
+
if run.agent._unknown:
|
|
328
|
+
return "failed", "outcome_unknown"
|
|
329
|
+
if run.stuck():
|
|
330
|
+
run.fail("Tool did not stop after cancellation")
|
|
331
|
+
return "cancelled" if run.token.cancelled else "failed", "tool_not_stopped"
|
|
332
|
+
if run.token.cancelled:
|
|
333
|
+
status, reason, _ = run.cancelled_outcome()
|
|
334
|
+
return status, reason
|
|
335
|
+
return "completed", "stop"
|
|
336
|
+
|
|
337
|
+
|
|
303
338
|
async def run_loop(run: Run, initial: list[Message]) -> tuple[str, str]:
|
|
304
339
|
context = run.context
|
|
305
340
|
assert context is not None
|
|
@@ -366,13 +401,8 @@ async def run_loop(run: Run, initial: list[Message]) -> tuple[str, str]:
|
|
|
366
401
|
else:
|
|
367
402
|
status, reason = "cancelled", run.token.reason or "aborted"
|
|
368
403
|
elif outcomes:
|
|
369
|
-
|
|
370
|
-
if max_calls is not None and tool_count + len(outcomes) > max_calls:
|
|
404
|
+
if exceeds_tool_budget(run, outcomes, tool_count):
|
|
371
405
|
status, reason = "limit_reached", "max_tool_calls"
|
|
372
|
-
for outcome in outcomes:
|
|
373
|
-
outcome.result = error_result(
|
|
374
|
-
"limit", "Entire tool batch exceeds remaining budget"
|
|
375
|
-
)
|
|
376
406
|
elif message.stop_reason == "length":
|
|
377
407
|
for outcome in outcomes:
|
|
378
408
|
await run.emit(
|
pi_python/messages.py
CHANGED
|
@@ -361,6 +361,63 @@ def message_from_dict(data: dict[str, Any]) -> Message:
|
|
|
361
361
|
raise MessageValidationError(str(exc)) from exc
|
|
362
362
|
|
|
363
363
|
|
|
364
|
+
def convert_application_tool_batch(message: CustomMessage) -> UserMessage:
|
|
365
|
+
"""Project built-in application feedback, never local details or raw results.
|
|
366
|
+
|
|
367
|
+
An allowlist also protects the default visibility boundary for imported records
|
|
368
|
+
with extra fields. Applications can override conversion with convert_to_llm.
|
|
369
|
+
"""
|
|
370
|
+
data = message.data
|
|
371
|
+
if not isinstance(data, dict) or not isinstance(data.get("calls"), list):
|
|
372
|
+
raise MessageValidationError("Invalid application tool batch")
|
|
373
|
+
calls = []
|
|
374
|
+
for call in data["calls"]:
|
|
375
|
+
if not isinstance(call, dict):
|
|
376
|
+
raise MessageValidationError("Invalid application tool call record")
|
|
377
|
+
keys = (
|
|
378
|
+
"id",
|
|
379
|
+
"name",
|
|
380
|
+
"arguments",
|
|
381
|
+
"prepared_arguments",
|
|
382
|
+
"origin",
|
|
383
|
+
"source",
|
|
384
|
+
"execution_status",
|
|
385
|
+
"is_error",
|
|
386
|
+
"error_code",
|
|
387
|
+
"content",
|
|
388
|
+
)
|
|
389
|
+
try:
|
|
390
|
+
visible = {key: call[key] for key in keys}
|
|
391
|
+
except KeyError as exc:
|
|
392
|
+
raise MessageValidationError("Incomplete application tool call record") from exc
|
|
393
|
+
validate_json(visible)
|
|
394
|
+
if (
|
|
395
|
+
visible["origin"] != "application"
|
|
396
|
+
or not isinstance(visible["id"], str)
|
|
397
|
+
or not visible["id"]
|
|
398
|
+
or not isinstance(visible["name"], str)
|
|
399
|
+
or not visible["name"]
|
|
400
|
+
or not isinstance(visible["arguments"], dict)
|
|
401
|
+
or (
|
|
402
|
+
visible["prepared_arguments"] is not None
|
|
403
|
+
and not isinstance(visible["prepared_arguments"], dict)
|
|
404
|
+
)
|
|
405
|
+
or (visible["source"] is not None and not isinstance(visible["source"], str))
|
|
406
|
+
or type(visible["is_error"]) is not bool
|
|
407
|
+
or visible["execution_status"]
|
|
408
|
+
not in ("not_started", "running", "succeeded", "failed", "cancelled", "unknown")
|
|
409
|
+
or (visible["error_code"] is not None and not isinstance(visible["error_code"], str))
|
|
410
|
+
):
|
|
411
|
+
raise MessageValidationError("Invalid application tool call record")
|
|
412
|
+
_blocks(visible["content"], images=True)
|
|
413
|
+
calls.append(visible)
|
|
414
|
+
return UserMessage(
|
|
415
|
+
"Application tool execution results (application-selected, not model tool calls):\n"
|
|
416
|
+
+ json.dumps({"calls": calls}, ensure_ascii=False, allow_nan=False),
|
|
417
|
+
timestamp=message.timestamp,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
364
421
|
def validate_history(messages: list[Message]) -> None:
|
|
365
422
|
pending: dict[str, str] = {}
|
|
366
423
|
for message in messages:
|
pi_python/run.py
CHANGED
|
@@ -15,7 +15,9 @@ from .errors import ConfigurationError
|
|
|
15
15
|
from .hooks import AgentConfigUpdate, Hooks, RunContext, TurnUpdate
|
|
16
16
|
from .messages import (
|
|
17
17
|
AssistantMessage,
|
|
18
|
+
CustomMessage,
|
|
18
19
|
Message,
|
|
20
|
+
ToolCall,
|
|
19
21
|
ToolResultMessage,
|
|
20
22
|
message_to_dict,
|
|
21
23
|
validate_history,
|
|
@@ -23,7 +25,7 @@ from .messages import (
|
|
|
23
25
|
from .errors import SubscriptionError
|
|
24
26
|
from .tools import ToolOutcome, error_result, invoke
|
|
25
27
|
from .transcript import declare_tool_changes
|
|
26
|
-
from .loop import abort_error, failure_message, run_loop
|
|
28
|
+
from .loop import abort_error, failure_message, run_loop, run_application_batch
|
|
27
29
|
|
|
28
30
|
if TYPE_CHECKING:
|
|
29
31
|
from .agent import Agent
|
|
@@ -58,11 +60,24 @@ def merge(target: Any, update: AgentConfigUpdate) -> None:
|
|
|
58
60
|
class Run:
|
|
59
61
|
"""State that lives for one run. The Agent keeps everything that outlives it."""
|
|
60
62
|
|
|
61
|
-
def __init__(
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
agent: Agent,
|
|
66
|
+
skip_initial_steering: bool = False,
|
|
67
|
+
*,
|
|
68
|
+
application_calls: list[ToolCall] | None = None,
|
|
69
|
+
source: str | None = None,
|
|
70
|
+
):
|
|
62
71
|
self.agent = agent
|
|
63
72
|
self.token = CancelToken()
|
|
64
73
|
self.id = str(uuid4())
|
|
65
74
|
self.turn = 0
|
|
75
|
+
self.application_batch = application_calls is not None
|
|
76
|
+
self.origin = "application" if self.application_batch else "model"
|
|
77
|
+
self.source = source
|
|
78
|
+
self.include_tool_origin = agent.include_tool_origin
|
|
79
|
+
self._limits = agent.limits
|
|
80
|
+
self._execution_mode = agent.execution_mode
|
|
66
81
|
defaults = agent._defaults
|
|
67
82
|
self.context = RunContext(
|
|
68
83
|
deepcopy(agent._messages),
|
|
@@ -74,8 +89,16 @@ class Run:
|
|
|
74
89
|
self.skip_initial_steering = skip_initial_steering
|
|
75
90
|
self.partial: Any = None
|
|
76
91
|
self.started = False # message_start was published for the current response
|
|
77
|
-
self.outcomes: list[ToolOutcome] = [
|
|
78
|
-
|
|
92
|
+
self.outcomes: list[ToolOutcome] = [
|
|
93
|
+
ToolOutcome(
|
|
94
|
+
deepcopy(call),
|
|
95
|
+
original_arguments=deepcopy(call.arguments),
|
|
96
|
+
)
|
|
97
|
+
for call in application_calls or []
|
|
98
|
+
]
|
|
99
|
+
for outcome in self.outcomes:
|
|
100
|
+
outcome._set_origin("application", source)
|
|
101
|
+
self.active_batch: list[ToolOutcome] = list(self.outcomes)
|
|
79
102
|
self._committed: set[int] = set()
|
|
80
103
|
self._ended: set[int] = set()
|
|
81
104
|
self._owned: set[asyncio.Future] = set()
|
|
@@ -95,11 +118,11 @@ class Run:
|
|
|
95
118
|
|
|
96
119
|
@property
|
|
97
120
|
def limits(self) -> RunLimits:
|
|
98
|
-
return self.agent.limits
|
|
121
|
+
return self._limits if self.application_batch else self.agent.limits
|
|
99
122
|
|
|
100
123
|
@property
|
|
101
124
|
def execution_mode(self) -> str:
|
|
102
|
-
return self.agent.execution_mode
|
|
125
|
+
return self._execution_mode if self.application_batch else self.agent.execution_mode
|
|
103
126
|
|
|
104
127
|
@property
|
|
105
128
|
def pending_calls(self) -> tuple[str, ...]:
|
|
@@ -199,7 +222,16 @@ class Run:
|
|
|
199
222
|
await asyncio.gather(cancel_wait, return_exceptions=True)
|
|
200
223
|
|
|
201
224
|
async def emit(self, kind: str, call_id: str | None = None, **data: Any) -> None:
|
|
202
|
-
|
|
225
|
+
if self.include_tool_origin and kind.startswith("tool_execution_"):
|
|
226
|
+
data.update(origin=self.origin, source=self.source)
|
|
227
|
+
await self.agent._events.emit(
|
|
228
|
+
kind,
|
|
229
|
+
self.id,
|
|
230
|
+
None if self.application_batch else self.turn,
|
|
231
|
+
data,
|
|
232
|
+
call_id,
|
|
233
|
+
self.await_owned,
|
|
234
|
+
)
|
|
203
235
|
|
|
204
236
|
async def hook(self, name: str, *args: Any) -> Any:
|
|
205
237
|
callback = getattr(self.hooks, name)
|
|
@@ -307,6 +339,8 @@ class Run:
|
|
|
307
339
|
result = outcome.result
|
|
308
340
|
outcome._settled = True
|
|
309
341
|
self._committed.add(id(outcome))
|
|
342
|
+
if self.application_batch:
|
|
343
|
+
return # One custom batch record is appended after all accepted calls settle.
|
|
310
344
|
message = ToolResultMessage(
|
|
311
345
|
outcome.call.id,
|
|
312
346
|
outcome.call.name,
|
|
@@ -330,6 +364,37 @@ class Run:
|
|
|
330
364
|
execution_status=outcome.execution_status,
|
|
331
365
|
)
|
|
332
366
|
|
|
367
|
+
async def append_application_batch(self) -> None:
|
|
368
|
+
calls = []
|
|
369
|
+
for outcome in self.outcomes:
|
|
370
|
+
result = outcome.result
|
|
371
|
+
assert result is not None # commit_outcome fills unresolved calls during cleanup.
|
|
372
|
+
calls.append(
|
|
373
|
+
{
|
|
374
|
+
"id": outcome.call.id,
|
|
375
|
+
"name": outcome.call.name,
|
|
376
|
+
"arguments": deepcopy(outcome.original_arguments),
|
|
377
|
+
"prepared_arguments": deepcopy(outcome.prepared_arguments),
|
|
378
|
+
"origin": outcome.origin,
|
|
379
|
+
"source": outcome.source,
|
|
380
|
+
"execution_status": outcome.execution_status,
|
|
381
|
+
"is_error": result.is_error,
|
|
382
|
+
"error_code": result.error_code,
|
|
383
|
+
"content": [asdict(block) for block in result.content],
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
await self.append(
|
|
387
|
+
CustomMessage(
|
|
388
|
+
"application_tool_batch",
|
|
389
|
+
{
|
|
390
|
+
"run_id": self.id,
|
|
391
|
+
"origin": self.origin,
|
|
392
|
+
"source": self.source,
|
|
393
|
+
"calls": calls,
|
|
394
|
+
},
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
|
|
333
398
|
async def drive(self, pending: list[Message]) -> RunResult:
|
|
334
399
|
agent = self.agent
|
|
335
400
|
self.driver_started = True
|
|
@@ -339,7 +404,11 @@ class Run:
|
|
|
339
404
|
# or aborted assistant message, followed by turn_end.
|
|
340
405
|
failure: AssistantMessage | None = None
|
|
341
406
|
try:
|
|
342
|
-
status, reason =
|
|
407
|
+
status, reason = (
|
|
408
|
+
await run_application_batch(self)
|
|
409
|
+
if self.application_batch
|
|
410
|
+
else await run_loop(self, pending)
|
|
411
|
+
)
|
|
343
412
|
except asyncio.CancelledError:
|
|
344
413
|
status, reason, failure = self.cancelled_outcome()
|
|
345
414
|
except Exception as exc:
|
|
@@ -366,6 +435,16 @@ class Run:
|
|
|
366
435
|
except Exception as exc:
|
|
367
436
|
status = "failed"
|
|
368
437
|
errors.append(f"{type(exc).__name__}: {exc}")
|
|
438
|
+
if self.application_batch:
|
|
439
|
+
failure = None # No model turn occurred, including on abort or failure.
|
|
440
|
+
try:
|
|
441
|
+
await self.append_application_batch()
|
|
442
|
+
except asyncio.CancelledError:
|
|
443
|
+
status = "cancelled" if status != "limit_reached" else status
|
|
444
|
+
errors.append("Batch message delivery cancelled")
|
|
445
|
+
except Exception as exc:
|
|
446
|
+
status = "failed"
|
|
447
|
+
errors.append(f"{type(exc).__name__}: {exc}")
|
|
369
448
|
tail = agent._messages[-1] if len(agent._messages) > self.start else None
|
|
370
449
|
if failure is not None and not (
|
|
371
450
|
isinstance(tail, AssistantMessage) and tail.stop_reason in {"error", "aborted"}
|
|
@@ -420,9 +499,10 @@ class Run:
|
|
|
420
499
|
self.fail(errors[-1])
|
|
421
500
|
await self.cleanup()
|
|
422
501
|
# Explicit config updates persist even if no further model request occurred.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
502
|
+
if not self.application_batch:
|
|
503
|
+
for update in agent._updates:
|
|
504
|
+
merge(agent._defaults, update)
|
|
505
|
+
agent._updates.clear()
|
|
426
506
|
usage: dict[str, Any] = {}
|
|
427
507
|
for m in agent._messages[self.start :]:
|
|
428
508
|
if isinstance(m, AssistantMessage):
|
pi_python/tools.py
CHANGED
|
@@ -348,8 +348,45 @@ def apply_result_update(result: ToolResult, update: ToolResultUpdate) -> None:
|
|
|
348
348
|
result.terminate = update.terminate
|
|
349
349
|
|
|
350
350
|
|
|
351
|
+
@dataclass(frozen=True)
|
|
352
|
+
class ToolInvocation:
|
|
353
|
+
"""An application-requested call; the Agent assigns its ID."""
|
|
354
|
+
|
|
355
|
+
name: str
|
|
356
|
+
arguments: dict[str, Any]
|
|
357
|
+
|
|
358
|
+
def __post_init__(self) -> None:
|
|
359
|
+
if not isinstance(self.name, str) or not self.name:
|
|
360
|
+
raise ConfigurationError("ToolInvocation requires a nonempty tool name")
|
|
361
|
+
validate_json(self.arguments)
|
|
362
|
+
if not isinstance(self.arguments, dict):
|
|
363
|
+
raise ConfigurationError("ToolInvocation arguments must be an object")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class _ToolOrigin:
|
|
367
|
+
"""Read-only provenance without changing legacy dataclass fields/asdict output.
|
|
368
|
+
|
|
369
|
+
Model calls need no per-instance storage. Application calls attach provenance
|
|
370
|
+
explicitly; deepcopy preserves it even though dataclasses.asdict excludes it.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
_origin = "model"
|
|
374
|
+
_source: str | None = None
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def origin(self) -> str:
|
|
378
|
+
return self._origin
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def source(self) -> str | None:
|
|
382
|
+
return self._source
|
|
383
|
+
|
|
384
|
+
def _set_origin(self, origin: str, source: str | None) -> None:
|
|
385
|
+
self._origin, self._source = origin, source
|
|
386
|
+
|
|
387
|
+
|
|
351
388
|
@dataclass
|
|
352
|
-
class ToolContext:
|
|
389
|
+
class ToolContext(_ToolOrigin):
|
|
353
390
|
run_id: str
|
|
354
391
|
call_id: str
|
|
355
392
|
cancel: CancelToken
|
|
@@ -449,7 +486,7 @@ class Tool:
|
|
|
449
486
|
|
|
450
487
|
|
|
451
488
|
@dataclass
|
|
452
|
-
class ToolOutcome:
|
|
489
|
+
class ToolOutcome(_ToolOrigin):
|
|
453
490
|
call: ToolCall
|
|
454
491
|
execution_status: str = "not_started"
|
|
455
492
|
original_arguments: dict[str, Any] = field(default_factory=dict)
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
pi_python/__init__.py,sha256
|
|
1
|
+
pi_python/__init__.py,sha256=-32DW8gNgcvPYNQJZUago0hrkgx_v4VZnOsr4BmG46M,3958
|
|
2
2
|
pi_python/_mcp_host.py,sha256=CXio-tiEeUCUfdl65tatwSrzIOYdgecdS00sO6tNy5k,8630
|
|
3
3
|
pi_python/_mcp_interaction.py,sha256=jPL-C8wLjBDV9tOdvGVFfXOl8-1FQIDJLq-MojlzXaM,43851
|
|
4
4
|
pi_python/_mcp_process.py,sha256=4szi5gMEP_0GUGOcV-oLPUuVQAUSb0ToAaiM979zHME,6324
|
|
5
5
|
pi_python/_mcp_sampling.py,sha256=BcWCrPgjsRC-GNx65fIB-JPq2WzWnoBK2w9iTnyPUww,12148
|
|
6
|
-
pi_python/_version.py,sha256=
|
|
7
|
-
pi_python/agent.py,sha256=
|
|
6
|
+
pi_python/_version.py,sha256=A_AARqtxTOj_AQTpjpgOxNx-UOBio5wYFfZ2mrdMKfs,23
|
|
7
|
+
pi_python/agent.py,sha256=86_mWt2oysz0Llv0LkQRnGKjYsloVElkQFnrkE5ZXv0,18206
|
|
8
8
|
pi_python/cancellation.py,sha256=DoQVdX7DcQdVLkl4OvhcpMhKZJEW0Kscl2-xxF6i6Y0,608
|
|
9
9
|
pi_python/errors.py,sha256=65lT0nYfzqxRfUlFG-Y9vgYxfDHEuFiBfgqmFnX9_O8,675
|
|
10
10
|
pi_python/estimate.py,sha256=dQQJ9UEGp57F3wO_CcU2oubgPe841ttgpUseK3Fu_0w,5048
|
|
11
|
-
pi_python/events.py,sha256=
|
|
12
|
-
pi_python/features.py,sha256=
|
|
11
|
+
pi_python/events.py,sha256=HFYaPB9-yjOvmuM2Ly7SgfWDMJJfX9e1Q7ethqOHSB8,4626
|
|
12
|
+
pi_python/features.py,sha256=V9Yhh1APuN8KaLMrAx8s00xeDLAF0ydh5jI5FCT9ZQg,1681
|
|
13
13
|
pi_python/function_tools.py,sha256=DO8fZD-n5i62w22zLIjblCOTwnG8N-HIoSUo4yOpRM4,18862
|
|
14
14
|
pi_python/hooks.py,sha256=jm6ep78NjdHxIPZvcOXTZIPxkfBMkffVTNzLvnHS9mM,1321
|
|
15
15
|
pi_python/limits.py,sha256=K1aP1cFH4fVhtWIXqpWMmpkB9AI0wK-bz3Xd2hxnOs4,1157
|
|
16
|
-
pi_python/loop.py,sha256=
|
|
16
|
+
pi_python/loop.py,sha256=nr3P6fXnrG21DFXPCGMGTKssE30jJmMoWi4bKIWxOu0,18904
|
|
17
17
|
pi_python/lowlevel.py,sha256=56wtwqD0Ur1AGAXGa7zclb1IpmRPFpSEss_nspkAdrI,5514
|
|
18
18
|
pi_python/mcp.py,sha256=f7VOiOeBwJ48SrowJWmqtH3d-iavrv2GZo2KH45qeTA,16713
|
|
19
|
-
pi_python/messages.py,sha256=
|
|
19
|
+
pi_python/messages.py,sha256=jUoQxR-UZbIQYWu257dGiCx8K5z_RV89fJpHgv4t-Ww,18068
|
|
20
20
|
pi_python/models.py,sha256=IejtaWvbURM9vv_2kkoDzfto2TR0OSLjuQeOh3IJZ1k,6094
|
|
21
21
|
pi_python/provider.py,sha256=k_VfJu5WOQla_LkglTDAq5s-77ejCj4YRTyhJ8onZBo,4259
|
|
22
22
|
pi_python/proxy.py,sha256=kpEUoE1oDkJW7VVaV72slN3uJUFgYR4Q6jtYOEe8LmQ,12159
|
|
23
23
|
pi_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
pi_python/queues.py,sha256=nQ5xRSA3MMOHMs_ZJnG9sT3-d_yKn72oSnxXqiKPrcI,3032
|
|
25
25
|
pi_python/recovery.py,sha256=z5ht-6FoKtC5BPEaSgXiRE2KVf94mZDiKMSxV0mkZ-4,8138
|
|
26
|
-
pi_python/run.py,sha256=
|
|
26
|
+
pi_python/run.py,sha256=S-mJ5xHVzVXdt8aBicRr4jU1-VZW1-QDi4Cru2hPgmE,21831
|
|
27
27
|
pi_python/stream.py,sha256=R3v-qjCHFQJhHQk83qATz7lC3yr8paJguaHwk8RoHow,10729
|
|
28
28
|
pi_python/sync.py,sha256=uTTX07hxe6TOCgwtfBxSJh2oJFDa2UklNmlCJ9g5-ss,5261
|
|
29
29
|
pi_python/tasks.py,sha256=UNPhb_HSTAaALcstV4X9T4q8JZTyMf90LjRR5bFw26M,4738
|
|
30
30
|
pi_python/testing.py,sha256=XfTSOkEv0CenqrBILI_T205jOhyitW53YQZ9yiMXir4,1046
|
|
31
|
-
pi_python/tools.py,sha256=
|
|
31
|
+
pi_python/tools.py,sha256=507d9JXMdn1fsoS1Y1TZIauEf4d570vDeRMfavb9itY,24417
|
|
32
32
|
pi_python/transcript.py,sha256=g6Tg8eiyMCAK43iLpcBv220MHBq8kO7Ty-6OYLCzw2A,6685
|
|
33
33
|
pi_python/data/models.json,sha256=oa_LGv7vaRrS-l7_Z7iu8f7WTyoJJLPhUQyptUz7OmE,77549
|
|
34
34
|
pi_python/plugins/__init__.py,sha256=FeYHYxgaOjKuq3QBBv1ezOyfORi5Fl2SdnImN_O0IrE,56156
|
|
@@ -43,8 +43,8 @@ pi_python/providers/completions.py,sha256=vcqoLfJ-9quMREEEAJcHYs3Oq-A0Sz8Q1NV1QL
|
|
|
43
43
|
pi_python/providers/oauth.py,sha256=ZY8j7P-I-R6hZ8sInkGao957KWNGsqfvMF0F-YzlSgU,22786
|
|
44
44
|
pi_python/providers/openai.py,sha256=p0WHDnojAYb7mUX8EbComwEU_XskcYoMhMIR07QlSfc,30196
|
|
45
45
|
pi_python/providers/transport.py,sha256=B1mGKlVEqYGVK1MlcA9opwRhnuUYGuaYuTslNC3O8xs,26772
|
|
46
|
-
pi_python_core-0.10.
|
|
47
|
-
pi_python_core-0.10.
|
|
48
|
-
pi_python_core-0.10.
|
|
49
|
-
pi_python_core-0.10.
|
|
50
|
-
pi_python_core-0.10.
|
|
46
|
+
pi_python_core-0.10.2.dist-info/METADATA,sha256=SGNj48Q5U-Q-Z0AVANvz5CQoJVcLqYkMjEkXZN3AFA4,8939
|
|
47
|
+
pi_python_core-0.10.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
48
|
+
pi_python_core-0.10.2.dist-info/licenses/LICENSE,sha256=BFf1vOw7OyEWBd-10aSQQv1jjzaGpBD-CZwkolrxPEg,1069
|
|
49
|
+
pi_python_core-0.10.2.dist-info/licenses/NOTICE,sha256=fJna16fm1ECHN7zVZhtyNmjLw8KGxGp8mL9blISF1K0,633
|
|
50
|
+
pi_python_core-0.10.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|