letta-nightly 0.13.0.dev20251030104218__py3-none-any.whl → 0.13.1.dev20251031234110__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +1 -1
- letta/adapters/simple_llm_stream_adapter.py +1 -0
- letta/agents/letta_agent_v2.py +8 -0
- letta/agents/letta_agent_v3.py +120 -27
- letta/agents/temporal/activities/__init__.py +25 -0
- letta/agents/temporal/activities/create_messages.py +26 -0
- letta/agents/temporal/activities/create_step.py +57 -0
- letta/agents/temporal/activities/example_activity.py +9 -0
- letta/agents/temporal/activities/execute_tool.py +130 -0
- letta/agents/temporal/activities/llm_request.py +114 -0
- letta/agents/temporal/activities/prepare_messages.py +27 -0
- letta/agents/temporal/activities/refresh_context.py +160 -0
- letta/agents/temporal/activities/summarize_conversation_history.py +77 -0
- letta/agents/temporal/activities/update_message_ids.py +25 -0
- letta/agents/temporal/activities/update_run.py +43 -0
- letta/agents/temporal/constants.py +59 -0
- letta/agents/temporal/temporal_agent_workflow.py +704 -0
- letta/agents/temporal/types.py +275 -0
- letta/constants.py +8 -0
- letta/errors.py +4 -0
- letta/functions/function_sets/base.py +0 -11
- letta/groups/helpers.py +7 -1
- letta/groups/sleeptime_multi_agent_v4.py +4 -3
- letta/interfaces/anthropic_streaming_interface.py +0 -1
- letta/interfaces/openai_streaming_interface.py +103 -100
- letta/llm_api/anthropic_client.py +57 -12
- letta/llm_api/bedrock_client.py +1 -0
- letta/llm_api/deepseek_client.py +3 -2
- letta/llm_api/google_vertex_client.py +1 -0
- letta/llm_api/groq_client.py +1 -0
- letta/llm_api/llm_client_base.py +15 -1
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +17 -3
- letta/llm_api/xai_client.py +1 -0
- letta/orm/organization.py +4 -0
- letta/orm/sqlalchemy_base.py +7 -0
- letta/otel/tracing.py +131 -4
- letta/schemas/agent_file.py +10 -10
- letta/schemas/block.py +22 -3
- letta/schemas/enums.py +21 -0
- letta/schemas/environment_variables.py +3 -2
- letta/schemas/group.py +3 -3
- letta/schemas/letta_response.py +36 -4
- letta/schemas/llm_batch_job.py +3 -3
- letta/schemas/llm_config.py +27 -3
- letta/schemas/mcp.py +3 -2
- letta/schemas/mcp_server.py +3 -2
- letta/schemas/message.py +167 -49
- letta/schemas/organization.py +2 -1
- letta/schemas/passage.py +2 -1
- letta/schemas/provider_trace.py +2 -1
- letta/schemas/providers/openrouter.py +1 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +3 -1
- letta/schemas/step_metrics.py +2 -1
- letta/schemas/tool_rule.py +2 -2
- letta/schemas/user.py +2 -1
- letta/server/rest_api/app.py +5 -1
- letta/server/rest_api/routers/v1/__init__.py +4 -0
- letta/server/rest_api/routers/v1/agents.py +71 -9
- letta/server/rest_api/routers/v1/blocks.py +7 -7
- letta/server/rest_api/routers/v1/groups.py +40 -0
- letta/server/rest_api/routers/v1/identities.py +2 -2
- letta/server/rest_api/routers/v1/internal_agents.py +31 -0
- letta/server/rest_api/routers/v1/internal_blocks.py +177 -0
- letta/server/rest_api/routers/v1/internal_runs.py +25 -1
- letta/server/rest_api/routers/v1/runs.py +2 -22
- letta/server/rest_api/routers/v1/tools.py +10 -0
- letta/server/server.py +5 -2
- letta/services/agent_manager.py +4 -4
- letta/services/archive_manager.py +16 -0
- letta/services/group_manager.py +44 -0
- letta/services/helpers/run_manager_helper.py +2 -2
- letta/services/lettuce/lettuce_client.py +148 -0
- letta/services/mcp/base_client.py +9 -3
- letta/services/run_manager.py +148 -37
- letta/services/source_manager.py +91 -3
- letta/services/step_manager.py +2 -3
- letta/services/streaming_service.py +52 -13
- letta/services/summarizer/summarizer.py +28 -2
- letta/services/tool_executor/builtin_tool_executor.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +2 -117
- letta/services/tool_schema_generator.py +2 -2
- letta/validators.py +21 -0
- {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/METADATA +1 -1
- {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/RECORD +89 -84
- letta/agent.py +0 -1758
- letta/cli/cli_load.py +0 -16
- letta/client/__init__.py +0 -0
- letta/client/streaming.py +0 -95
- letta/client/utils.py +0 -78
- letta/functions/async_composio_toolset.py +0 -109
- letta/functions/composio_helpers.py +0 -96
- letta/helpers/composio_helpers.py +0 -38
- letta/orm/job_messages.py +0 -33
- letta/schemas/providers.py +0 -1617
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -132
- letta/services/tool_executor/composio_tool_executor.py +0 -57
- {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/WHEEL +0 -0
- {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from temporalio import workflow
|
|
5
|
+
from temporalio.exceptions import ActivityError, ApplicationError
|
|
6
|
+
|
|
7
|
+
from letta.agents.helpers import _load_last_function_response, _maybe_get_approval_messages, generate_step_id
|
|
8
|
+
from letta.agents.temporal.activities.execute_tool import deserialize_func_return, is_serialized_exception
|
|
9
|
+
from letta.agents.temporal.constants import (
|
|
10
|
+
CREATE_MESSAGES_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
11
|
+
CREATE_MESSAGES_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
12
|
+
CREATE_STEP_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
13
|
+
CREATE_STEP_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
14
|
+
LLM_ACTIVITY_RETRY_POLICY,
|
|
15
|
+
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
16
|
+
LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
17
|
+
PREPARE_MESSAGES_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
18
|
+
PREPARE_MESSAGES_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
19
|
+
REFRESH_CONTEXT_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
20
|
+
REFRESH_CONTEXT_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
21
|
+
SUMMARIZE_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
22
|
+
SUMMARIZE_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
23
|
+
TOOL_EXECUTION_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
24
|
+
TOOL_EXECUTION_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
25
|
+
UPDATE_RUN_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
26
|
+
UPDATE_RUN_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
27
|
+
)
|
|
28
|
+
from letta.constants import REQUEST_HEARTBEAT_PARAM
|
|
29
|
+
from letta.helpers import ToolRulesSolver
|
|
30
|
+
from letta.helpers.tool_execution_helper import enable_strict_mode
|
|
31
|
+
from letta.schemas.agent import AgentState
|
|
32
|
+
from letta.schemas.enums import RunStatus
|
|
33
|
+
from letta.schemas.letta_message import MessageType
|
|
34
|
+
from letta.schemas.letta_message_content import (
|
|
35
|
+
OmittedReasoningContent,
|
|
36
|
+
ReasoningContent,
|
|
37
|
+
RedactedReasoningContent,
|
|
38
|
+
TextContent,
|
|
39
|
+
)
|
|
40
|
+
from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
|
|
41
|
+
from letta.schemas.message import Message
|
|
42
|
+
from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall, UsageStatistics
|
|
43
|
+
from letta.schemas.tool_execution_result import ToolExecutionResult
|
|
44
|
+
from letta.schemas.usage import LettaUsageStatistics
|
|
45
|
+
from letta.schemas.user import User
|
|
46
|
+
from letta.server.rest_api.utils import create_letta_messages_from_llm_response
|
|
47
|
+
from letta.services.helpers.tool_parser_helper import runtime_override_tool_json_schema
|
|
48
|
+
|
|
49
|
+
# Import activity, passing it through the sandbox without reloading the module
|
|
50
|
+
with workflow.unsafe.imports_passed_through():
|
|
51
|
+
from letta.agents.helpers import _build_rule_violation_result, _load_last_function_response, _pop_heartbeat, _safe_load_tool_call_str
|
|
52
|
+
from letta.agents.temporal.activities import (
|
|
53
|
+
create_messages,
|
|
54
|
+
create_step,
|
|
55
|
+
execute_tool,
|
|
56
|
+
llm_request,
|
|
57
|
+
prepare_messages,
|
|
58
|
+
refresh_context_and_system_message,
|
|
59
|
+
summarize_conversation_history,
|
|
60
|
+
update_message_ids,
|
|
61
|
+
update_run,
|
|
62
|
+
)
|
|
63
|
+
from letta.agents.temporal.types import (
|
|
64
|
+
CreateMessagesParams,
|
|
65
|
+
CreateStepParams,
|
|
66
|
+
ExecuteToolParams,
|
|
67
|
+
ExecuteToolResult,
|
|
68
|
+
FinalResult,
|
|
69
|
+
InnerStepResult,
|
|
70
|
+
LLMCallResult,
|
|
71
|
+
LLMRequestParams,
|
|
72
|
+
PreparedMessages,
|
|
73
|
+
RefreshContextParams,
|
|
74
|
+
RefreshContextResult,
|
|
75
|
+
SummarizeParams,
|
|
76
|
+
UpdateMessageIdsParams,
|
|
77
|
+
UpdateMessageIdsResult,
|
|
78
|
+
UpdateRunParams,
|
|
79
|
+
WorkflowInputParams,
|
|
80
|
+
)
|
|
81
|
+
from letta.constants import NON_USER_MSG_PREFIX
|
|
82
|
+
from letta.local_llm.constants import INNER_THOUGHTS_KWARG
|
|
83
|
+
from letta.log import get_logger
|
|
84
|
+
from letta.server.rest_api.utils import create_approval_request_message_from_llm_response
|
|
85
|
+
from letta.settings import summarizer_settings
|
|
86
|
+
from letta.system import package_function_response
|
|
87
|
+
from letta.utils import validate_function_response
|
|
88
|
+
|
|
89
|
+
logger = get_logger(__name__)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_workflow_time_ns() -> int:
|
|
93
|
+
"""Get current workflow time in nanoseconds for deterministic timing."""
|
|
94
|
+
return int(workflow.now().timestamp() * 1e9)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@workflow.defn
|
|
98
|
+
class TemporalAgentWorkflow:
|
|
99
|
+
@workflow.run
|
|
100
|
+
async def run(self, params: WorkflowInputParams) -> FinalResult:
|
|
101
|
+
# Capture workflow start time for duration tracking
|
|
102
|
+
workflow_start_ns = get_workflow_time_ns()
|
|
103
|
+
|
|
104
|
+
# Initialize workflow state
|
|
105
|
+
agent_state = params.agent_state # track mutable agent state throughout workflow
|
|
106
|
+
tool_rules_solver = ToolRulesSolver(tool_rules=agent_state.tool_rules)
|
|
107
|
+
# Initialize tracking variables
|
|
108
|
+
usage = LettaUsageStatistics()
|
|
109
|
+
stop_reason = StopReasonType.end_turn
|
|
110
|
+
response_messages = []
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
# Prepare messages (context + new input), no persistence
|
|
114
|
+
prepared: PreparedMessages = await workflow.execute_activity(
|
|
115
|
+
prepare_messages,
|
|
116
|
+
params,
|
|
117
|
+
start_to_close_timeout=PREPARE_MESSAGES_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
118
|
+
schedule_to_close_timeout=PREPARE_MESSAGES_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
119
|
+
)
|
|
120
|
+
combined_messages = prepared.in_context_messages + prepared.input_messages_to_persist
|
|
121
|
+
input_messages = prepared.input_messages_to_persist
|
|
122
|
+
|
|
123
|
+
# Main agent loop - execute steps until max_steps or stop condition
|
|
124
|
+
for step_index in range(params.max_steps):
|
|
125
|
+
remaining_turns = params.max_steps - step_index - 1
|
|
126
|
+
|
|
127
|
+
# Execute single step
|
|
128
|
+
step_result = await self.inner_step(
|
|
129
|
+
agent_state=agent_state,
|
|
130
|
+
tool_rules_solver=tool_rules_solver,
|
|
131
|
+
messages=combined_messages,
|
|
132
|
+
input_messages_to_persist=input_messages,
|
|
133
|
+
use_assistant_message=params.use_assistant_message,
|
|
134
|
+
include_return_message_types=params.include_return_message_types,
|
|
135
|
+
actor=params.actor,
|
|
136
|
+
remaining_turns=remaining_turns,
|
|
137
|
+
run_id=params.run_id,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Update agent state from the step result
|
|
141
|
+
agent_state = step_result.agent_state
|
|
142
|
+
|
|
143
|
+
# Update aggregate usage
|
|
144
|
+
usage.step_count += step_result.usage.step_count
|
|
145
|
+
usage.completion_tokens += step_result.usage.completion_tokens
|
|
146
|
+
usage.prompt_tokens += step_result.usage.prompt_tokens
|
|
147
|
+
usage.total_tokens += step_result.usage.total_tokens
|
|
148
|
+
|
|
149
|
+
# Update stop reason from step result
|
|
150
|
+
if step_result.stop_reason is not None:
|
|
151
|
+
stop_reason = step_result.stop_reason
|
|
152
|
+
response_messages.extend(step_result.response_messages)
|
|
153
|
+
combined_messages.extend(step_result.response_messages)
|
|
154
|
+
|
|
155
|
+
# Check if we should continue
|
|
156
|
+
if not step_result.should_continue:
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
input_messages = [] # Only need to persist the input messages for the first step
|
|
160
|
+
|
|
161
|
+
# convert to letta messages from Message objs
|
|
162
|
+
letta_messages = Message.to_letta_messages_from_list(
|
|
163
|
+
response_messages,
|
|
164
|
+
use_assistant_message=params.use_assistant_message,
|
|
165
|
+
reverse=False,
|
|
166
|
+
)
|
|
167
|
+
# Finalize run with all messages to avoid partial metadata overwrites
|
|
168
|
+
# Determine final stop reason and run status
|
|
169
|
+
try:
|
|
170
|
+
if isinstance(stop_reason, StopReasonType):
|
|
171
|
+
final_stop_reason_type = stop_reason
|
|
172
|
+
elif isinstance(stop_reason, LettaStopReason):
|
|
173
|
+
final_stop_reason_type = stop_reason.stop_reason
|
|
174
|
+
elif isinstance(stop_reason, str):
|
|
175
|
+
final_stop_reason_type = StopReasonType(stop_reason)
|
|
176
|
+
else:
|
|
177
|
+
final_stop_reason_type = StopReasonType.end_turn
|
|
178
|
+
except Exception:
|
|
179
|
+
final_stop_reason_type = StopReasonType.end_turn
|
|
180
|
+
|
|
181
|
+
# Calculate total duration
|
|
182
|
+
workflow_end_ns = get_workflow_time_ns()
|
|
183
|
+
total_duration_ns = workflow_end_ns - workflow_start_ns
|
|
184
|
+
|
|
185
|
+
await workflow.execute_activity(
|
|
186
|
+
update_run,
|
|
187
|
+
UpdateRunParams(
|
|
188
|
+
run_id=params.run_id,
|
|
189
|
+
actor=params.actor,
|
|
190
|
+
run_status=final_stop_reason_type.run_status,
|
|
191
|
+
stop_reason=LettaStopReason(stop_reason=final_stop_reason_type),
|
|
192
|
+
# Pass all messages accumulated across the workflow
|
|
193
|
+
persisted_messages=response_messages,
|
|
194
|
+
usage=usage,
|
|
195
|
+
total_duration_ns=total_duration_ns,
|
|
196
|
+
),
|
|
197
|
+
start_to_close_timeout=UPDATE_RUN_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
198
|
+
schedule_to_close_timeout=UPDATE_RUN_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
199
|
+
)
|
|
200
|
+
return FinalResult(
|
|
201
|
+
stop_reason=stop_reason.value if isinstance(stop_reason, StopReasonType) else str(stop_reason),
|
|
202
|
+
usage=usage,
|
|
203
|
+
messages=letta_messages,
|
|
204
|
+
)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
final_stop_reason_type = self._map_exception_to_stop_reason(e)
|
|
207
|
+
try:
|
|
208
|
+
# Calculate total duration on exception path
|
|
209
|
+
workflow_end_ns = get_workflow_time_ns()
|
|
210
|
+
total_duration_ns = workflow_end_ns - workflow_start_ns
|
|
211
|
+
|
|
212
|
+
await workflow.execute_activity(
|
|
213
|
+
update_run,
|
|
214
|
+
UpdateRunParams(
|
|
215
|
+
run_id=params.run_id,
|
|
216
|
+
actor=params.actor,
|
|
217
|
+
run_status=final_stop_reason_type.run_status,
|
|
218
|
+
stop_reason=LettaStopReason(stop_reason=final_stop_reason_type),
|
|
219
|
+
persisted_messages=response_messages,
|
|
220
|
+
usage=usage,
|
|
221
|
+
total_duration_ns=total_duration_ns,
|
|
222
|
+
),
|
|
223
|
+
start_to_close_timeout=UPDATE_RUN_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
224
|
+
schedule_to_close_timeout=UPDATE_RUN_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
225
|
+
)
|
|
226
|
+
except Exception as update_err:
|
|
227
|
+
logger.error(f"Failed to update run {params.run_id} after workflow error: {update_err}")
|
|
228
|
+
raise
|
|
229
|
+
|
|
230
|
+
async def inner_step(
|
|
231
|
+
self,
|
|
232
|
+
agent_state: AgentState,
|
|
233
|
+
tool_rules_solver: ToolRulesSolver,
|
|
234
|
+
messages: list[Message],
|
|
235
|
+
actor: User,
|
|
236
|
+
input_messages_to_persist: list[Message] | None = None,
|
|
237
|
+
use_assistant_message: bool = True,
|
|
238
|
+
include_return_message_types: list[MessageType] | None = None,
|
|
239
|
+
request_start_timestamp_ns: int | None = None,
|
|
240
|
+
remaining_turns: int = -1,
|
|
241
|
+
run_id: str | None = None,
|
|
242
|
+
) -> InnerStepResult:
|
|
243
|
+
# Initialize step state
|
|
244
|
+
usage = LettaUsageStatistics()
|
|
245
|
+
stop_reason = StopReasonType.end_turn
|
|
246
|
+
tool_call = None
|
|
247
|
+
reasoning_content = None
|
|
248
|
+
step_id = None
|
|
249
|
+
|
|
250
|
+
# Track step start time using workflow.now() for deterministic time
|
|
251
|
+
step_start_time_ns = get_workflow_time_ns()
|
|
252
|
+
|
|
253
|
+
last_function_response = _load_last_function_response(messages)
|
|
254
|
+
allowed_tools = await self._get_valid_tools(
|
|
255
|
+
agent_state=agent_state, tool_rules_solver=tool_rules_solver, last_function_response=last_function_response
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
approval_request, approval_response = _maybe_get_approval_messages(messages)
|
|
259
|
+
|
|
260
|
+
# TODO: Need to check approval functionality
|
|
261
|
+
if approval_request and approval_response:
|
|
262
|
+
tool_call = approval_request.tool_calls[0]
|
|
263
|
+
reasoning_content = approval_request.content
|
|
264
|
+
step_id = approval_request.step_id
|
|
265
|
+
else:
|
|
266
|
+
# TODO: check for run cancellation if run_id provided
|
|
267
|
+
|
|
268
|
+
# Generate new step ID
|
|
269
|
+
step_id = generate_step_id(workflow.uuid4())
|
|
270
|
+
|
|
271
|
+
# TODO: step checkpoint start (logging/telemetry)
|
|
272
|
+
|
|
273
|
+
refresh_result: RefreshContextResult = await workflow.execute_activity(
|
|
274
|
+
refresh_context_and_system_message,
|
|
275
|
+
RefreshContextParams(
|
|
276
|
+
agent_state=agent_state,
|
|
277
|
+
in_context_messages=messages,
|
|
278
|
+
tool_rules_solver=tool_rules_solver,
|
|
279
|
+
actor=actor,
|
|
280
|
+
),
|
|
281
|
+
start_to_close_timeout=REFRESH_CONTEXT_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
282
|
+
schedule_to_close_timeout=REFRESH_CONTEXT_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
283
|
+
)
|
|
284
|
+
refreshed_messages = refresh_result.messages
|
|
285
|
+
agent_state = refresh_result.agent_state
|
|
286
|
+
|
|
287
|
+
force_tool_call = allowed_tools[0]["name"] if len(allowed_tools) == 1 else None
|
|
288
|
+
requires_approval_tools = (
|
|
289
|
+
tool_rules_solver.get_requires_approval_tools(set([t["name"] for t in allowed_tools])) if allowed_tools else None
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# LLM request with Temporal native retries; on context window overflow,
|
|
293
|
+
# perform workflow-level summarization before retrying with updated input.
|
|
294
|
+
call_result: LLMCallResult | None = None
|
|
295
|
+
for summarize_attempt in range(summarizer_settings.max_summarizer_retries + 1):
|
|
296
|
+
try:
|
|
297
|
+
# TODO: step checkpoint for LLM request start
|
|
298
|
+
|
|
299
|
+
call_result = await workflow.execute_activity(
|
|
300
|
+
llm_request,
|
|
301
|
+
LLMRequestParams(
|
|
302
|
+
agent_state=agent_state,
|
|
303
|
+
messages=refreshed_messages,
|
|
304
|
+
allowed_tools=allowed_tools,
|
|
305
|
+
force_tool_call=force_tool_call,
|
|
306
|
+
requires_approval_tools=requires_approval_tools,
|
|
307
|
+
actor=actor,
|
|
308
|
+
step_id=step_id,
|
|
309
|
+
use_assistant_message=use_assistant_message,
|
|
310
|
+
),
|
|
311
|
+
start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
312
|
+
schedule_to_close_timeout=LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
313
|
+
retry_policy=LLM_ACTIVITY_RETRY_POLICY,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Capture LLM timing from the result
|
|
317
|
+
llm_request_ns = call_result.llm_request_ns
|
|
318
|
+
|
|
319
|
+
# If successful, break out of summarization retry loop
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
except (ApplicationError, ActivityError) as e:
|
|
323
|
+
app_err = e.cause if isinstance(e, ActivityError) else e
|
|
324
|
+
error_type = getattr(app_err, "type", None)
|
|
325
|
+
|
|
326
|
+
# If context window exceeded, summarize then retry (up to max)
|
|
327
|
+
if (
|
|
328
|
+
error_type
|
|
329
|
+
and "ContextWindowExceededError" in error_type
|
|
330
|
+
and summarize_attempt < summarizer_settings.max_summarizer_retries
|
|
331
|
+
):
|
|
332
|
+
refreshed_messages = await workflow.execute_activity(
|
|
333
|
+
summarize_conversation_history,
|
|
334
|
+
SummarizeParams(
|
|
335
|
+
agent_state=agent_state,
|
|
336
|
+
in_context_messages=refreshed_messages,
|
|
337
|
+
new_letta_messages=[],
|
|
338
|
+
actor=actor,
|
|
339
|
+
force=True,
|
|
340
|
+
),
|
|
341
|
+
start_to_close_timeout=SUMMARIZE_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
342
|
+
schedule_to_close_timeout=SUMMARIZE_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
343
|
+
)
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
# Map error to stop reasons similar to non‑Temporal implementation
|
|
347
|
+
if error_type in ("ValueError", "LLMJSONParsingError"):
|
|
348
|
+
stop_reason = StopReasonType.invalid_llm_response
|
|
349
|
+
else:
|
|
350
|
+
stop_reason = StopReasonType.llm_api_error
|
|
351
|
+
# Exit summarization loop and finish step with stop_reason
|
|
352
|
+
break
|
|
353
|
+
|
|
354
|
+
# If LLM call ultimately failed, finish step early with mapped stop_reason
|
|
355
|
+
if call_result is None:
|
|
356
|
+
response_messages = []
|
|
357
|
+
should_continue = False
|
|
358
|
+
return InnerStepResult(
|
|
359
|
+
stop_reason=stop_reason,
|
|
360
|
+
usage=usage,
|
|
361
|
+
should_continue=should_continue,
|
|
362
|
+
response_messages=response_messages,
|
|
363
|
+
agent_state=agent_state,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# TODO: step checkpoint for LLM request finish
|
|
367
|
+
|
|
368
|
+
# Update usage stats (pure)
|
|
369
|
+
usage.step_count += 1
|
|
370
|
+
usage.completion_tokens += call_result.usage.completion_tokens
|
|
371
|
+
usage.prompt_tokens += call_result.usage.prompt_tokens
|
|
372
|
+
usage.total_tokens += call_result.usage.total_tokens
|
|
373
|
+
|
|
374
|
+
# Validate tool call exists
|
|
375
|
+
tool_call = call_result.tool_call
|
|
376
|
+
if tool_call is None:
|
|
377
|
+
stop_reason = StopReasonType.no_tool_call.value
|
|
378
|
+
should_continue = False
|
|
379
|
+
response_messages = []
|
|
380
|
+
return InnerStepResult(
|
|
381
|
+
stop_reason=stop_reason,
|
|
382
|
+
usage=usage,
|
|
383
|
+
should_continue=should_continue,
|
|
384
|
+
response_messages=response_messages,
|
|
385
|
+
agent_state=agent_state,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Handle the AI response (execute tool, create messages, determine continuation)
|
|
389
|
+
persisted_messages, should_continue, stop_reason, recent_last_function_response = await self._handle_ai_response(
|
|
390
|
+
tool_call=tool_call,
|
|
391
|
+
valid_tool_names=[t["name"] for t in allowed_tools],
|
|
392
|
+
agent_state=agent_state,
|
|
393
|
+
tool_rules_solver=tool_rules_solver,
|
|
394
|
+
actor=actor,
|
|
395
|
+
step_id=step_id,
|
|
396
|
+
reasoning_content=call_result.reasoning_content,
|
|
397
|
+
pre_computed_assistant_message_id=call_result.assistant_message_id,
|
|
398
|
+
initial_messages=input_messages_to_persist,
|
|
399
|
+
is_approval=approval_response.approve if approval_response is not None else False,
|
|
400
|
+
is_denial=(approval_response.approve == False) if approval_response is not None else False,
|
|
401
|
+
denial_reason=approval_response.denial_reason if approval_response is not None else None,
|
|
402
|
+
is_final_step=(remaining_turns == 0),
|
|
403
|
+
usage=usage,
|
|
404
|
+
run_id=run_id,
|
|
405
|
+
step_start_time_ns=step_start_time_ns,
|
|
406
|
+
llm_request_ns=llm_request_ns,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if recent_last_function_response:
|
|
410
|
+
# TODO: This doesn't get used, so we can skip parsing this in the above function
|
|
411
|
+
last_function_response = recent_last_function_response
|
|
412
|
+
|
|
413
|
+
# persist approval responses immediately to prevent agent from getting into a bad state
|
|
414
|
+
if (
|
|
415
|
+
len(input_messages_to_persist) == 1
|
|
416
|
+
and input_messages_to_persist[0].role == "approval"
|
|
417
|
+
and persisted_messages[0].role == "approval"
|
|
418
|
+
and persisted_messages[1].role == "tool"
|
|
419
|
+
):
|
|
420
|
+
# update message ids immediately for approval persistence
|
|
421
|
+
message_ids = agent_state.message_ids + [m.id for m in persisted_messages[:2]]
|
|
422
|
+
|
|
423
|
+
# call activity to persist the updated message ids
|
|
424
|
+
update_result: UpdateMessageIdsResult = await workflow.execute_activity(
|
|
425
|
+
update_message_ids,
|
|
426
|
+
UpdateMessageIdsParams(
|
|
427
|
+
agent_id=agent_state.id,
|
|
428
|
+
message_ids=message_ids,
|
|
429
|
+
actor=actor,
|
|
430
|
+
),
|
|
431
|
+
start_to_close_timeout=CREATE_MESSAGES_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
432
|
+
schedule_to_close_timeout=CREATE_MESSAGES_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# update agent state from the activity result
|
|
436
|
+
agent_state = update_result.agent_state
|
|
437
|
+
|
|
438
|
+
# TODO: process response messages for streaming/non-streaming
|
|
439
|
+
# - yield appropriate messages based on include_return_message_types
|
|
440
|
+
|
|
441
|
+
# TODO: step checkpoint finish
|
|
442
|
+
|
|
443
|
+
# Update response messages with the persisted messages
|
|
444
|
+
response_messages = persisted_messages
|
|
445
|
+
|
|
446
|
+
return InnerStepResult(
|
|
447
|
+
stop_reason=stop_reason,
|
|
448
|
+
usage=usage,
|
|
449
|
+
should_continue=should_continue,
|
|
450
|
+
response_messages=response_messages,
|
|
451
|
+
agent_state=agent_state,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
async def _handle_ai_response(
|
|
455
|
+
self,
|
|
456
|
+
tool_call: ToolCall,
|
|
457
|
+
valid_tool_names: list[str],
|
|
458
|
+
agent_state: AgentState,
|
|
459
|
+
tool_rules_solver: ToolRulesSolver,
|
|
460
|
+
actor: User,
|
|
461
|
+
step_id: str | None = None,
|
|
462
|
+
reasoning_content: list[TextContent | ReasoningContent | RedactedReasoningContent | OmittedReasoningContent] | None = None,
|
|
463
|
+
pre_computed_assistant_message_id: str | None = None,
|
|
464
|
+
initial_messages: list[Message] | None = None,
|
|
465
|
+
is_approval: bool = False,
|
|
466
|
+
is_denial: bool = False,
|
|
467
|
+
denial_reason: str | None = None,
|
|
468
|
+
is_final_step: bool = False,
|
|
469
|
+
usage: UsageStatistics | None = None,
|
|
470
|
+
run_id: str | None = None,
|
|
471
|
+
step_start_time_ns: int | None = None,
|
|
472
|
+
llm_request_ns: int | None = None,
|
|
473
|
+
) -> tuple[list[Message], bool, LettaStopReason | None, str | None]:
|
|
474
|
+
"""
|
|
475
|
+
Handle the AI response by executing the tool call, creating messages,
|
|
476
|
+
and determining whether to continue stepping.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
tuple[list[Message], bool, LettaStopReason | None]: (persisted_messages, should_continue, stop_reason)
|
|
480
|
+
"""
|
|
481
|
+
# Initialize default
|
|
482
|
+
initial_messages = initial_messages or []
|
|
483
|
+
|
|
484
|
+
# Parse and validate the tool-call envelope
|
|
485
|
+
tool_call_id = tool_call.id or f"call_{uuid.uuid4().hex[:8]}"
|
|
486
|
+
tool_call_name = tool_call.function.name
|
|
487
|
+
tool_args = _safe_load_tool_call_str(tool_call.function.arguments)
|
|
488
|
+
request_heartbeat = _pop_heartbeat(tool_args)
|
|
489
|
+
tool_args.pop(INNER_THOUGHTS_KWARG, None)
|
|
490
|
+
|
|
491
|
+
# Handle denial flow
|
|
492
|
+
if is_denial:
|
|
493
|
+
continue_stepping = True
|
|
494
|
+
stop_reason = None
|
|
495
|
+
tool_call_messages = create_letta_messages_from_llm_response(
|
|
496
|
+
agent_id=agent_state.id,
|
|
497
|
+
model=agent_state.llm_config.model,
|
|
498
|
+
function_name=tool_call.function.name,
|
|
499
|
+
function_arguments={},
|
|
500
|
+
tool_execution_result=ToolExecutionResult(status="error"),
|
|
501
|
+
tool_call_id=tool_call_id,
|
|
502
|
+
function_response=f"Error: request to call tool denied. User reason: {denial_reason}",
|
|
503
|
+
timezone=agent_state.timezone,
|
|
504
|
+
continue_stepping=continue_stepping,
|
|
505
|
+
heartbeat_reason=f"{NON_USER_MSG_PREFIX}Continuing: user denied request to call tool.",
|
|
506
|
+
reasoning_content=reasoning_content,
|
|
507
|
+
pre_computed_assistant_message_id=pre_computed_assistant_message_id,
|
|
508
|
+
is_approval_response=True,
|
|
509
|
+
step_id=step_id,
|
|
510
|
+
run_id=run_id,
|
|
511
|
+
)
|
|
512
|
+
messages_to_persist = initial_messages + tool_call_messages
|
|
513
|
+
return messages_to_persist, continue_stepping, stop_reason, None
|
|
514
|
+
|
|
515
|
+
# Handle approval request flow
|
|
516
|
+
if not is_approval and tool_rules_solver.is_requires_approval_tool(tool_call_name):
|
|
517
|
+
tool_args[REQUEST_HEARTBEAT_PARAM] = request_heartbeat
|
|
518
|
+
approval_messages = create_approval_request_message_from_llm_response(
|
|
519
|
+
agent_id=agent_state.id,
|
|
520
|
+
model=agent_state.llm_config.model,
|
|
521
|
+
requested_tool_calls=[
|
|
522
|
+
ToolCall(id=tool_call_id, function=FunctionCall(name=tool_call_name, arguments=json.dumps(tool_args)))
|
|
523
|
+
],
|
|
524
|
+
reasoning_content=reasoning_content,
|
|
525
|
+
pre_computed_assistant_message_id=pre_computed_assistant_message_id,
|
|
526
|
+
step_id=step_id,
|
|
527
|
+
run_id=run_id,
|
|
528
|
+
)
|
|
529
|
+
messages_to_persist = initial_messages + approval_messages
|
|
530
|
+
continue_stepping = False
|
|
531
|
+
stop_reason = LettaStopReason(stop_reason=StopReasonType.requires_approval.value)
|
|
532
|
+
return messages_to_persist, continue_stepping, stop_reason, None
|
|
533
|
+
|
|
534
|
+
# Execute tool if tool rules allow
|
|
535
|
+
tool_execution_ns = None
|
|
536
|
+
tool_rule_violated = tool_call_name not in valid_tool_names and not is_approval
|
|
537
|
+
if tool_rule_violated:
|
|
538
|
+
tool_result = _build_rule_violation_result(tool_call_name, valid_tool_names, tool_rules_solver)
|
|
539
|
+
else:
|
|
540
|
+
execution: ExecuteToolResult = await workflow.execute_activity(
|
|
541
|
+
execute_tool,
|
|
542
|
+
ExecuteToolParams(
|
|
543
|
+
tool_name=tool_call_name,
|
|
544
|
+
tool_args=tool_args,
|
|
545
|
+
agent_state=agent_state,
|
|
546
|
+
actor=actor,
|
|
547
|
+
step_id=step_id,
|
|
548
|
+
),
|
|
549
|
+
start_to_close_timeout=TOOL_EXECUTION_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
550
|
+
schedule_to_close_timeout=TOOL_EXECUTION_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# Capture tool execution timing
|
|
554
|
+
tool_execution_ns = execution.execution_time_ns
|
|
555
|
+
|
|
556
|
+
# Deserialize any serialized exceptions for post processing
|
|
557
|
+
if is_serialized_exception(execution.tool_execution_result.func_return):
|
|
558
|
+
execution.tool_execution_result.func_return = deserialize_func_return(execution.tool_execution_result.func_return)
|
|
559
|
+
tool_result = execution.tool_execution_result
|
|
560
|
+
|
|
561
|
+
# Prepare the function-response payload
|
|
562
|
+
truncate = tool_call_name not in {"conversation_search", "conversation_search_date", "archival_memory_search"}
|
|
563
|
+
return_char_limit = next(
|
|
564
|
+
(t.return_char_limit for t in agent_state.tools if t.name == tool_call_name),
|
|
565
|
+
None,
|
|
566
|
+
)
|
|
567
|
+
function_response_string = validate_function_response(
|
|
568
|
+
tool_result.func_return,
|
|
569
|
+
return_char_limit=return_char_limit,
|
|
570
|
+
truncate=truncate,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# Package the function response (for last_function_response tracking)
|
|
574
|
+
last_function_response = package_function_response(
|
|
575
|
+
was_success=tool_result.success_flag,
|
|
576
|
+
response_string=function_response_string,
|
|
577
|
+
timezone=agent_state.timezone,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
# Decide whether to continue stepping
|
|
581
|
+
continue_stepping = request_heartbeat
|
|
582
|
+
heartbeat_reason = None
|
|
583
|
+
stop_reason = None
|
|
584
|
+
|
|
585
|
+
if tool_rule_violated:
|
|
586
|
+
continue_stepping = True
|
|
587
|
+
heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing: tool rule violation."
|
|
588
|
+
else:
|
|
589
|
+
tool_rules_solver.register_tool_call(tool_call_name)
|
|
590
|
+
|
|
591
|
+
if tool_rules_solver.is_terminal_tool(tool_call_name):
|
|
592
|
+
if continue_stepping:
|
|
593
|
+
stop_reason = LettaStopReason(stop_reason=StopReasonType.tool_rule.value)
|
|
594
|
+
continue_stepping = False
|
|
595
|
+
elif tool_rules_solver.has_children_tools(tool_call_name):
|
|
596
|
+
continue_stepping = True
|
|
597
|
+
heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing: child tool rule."
|
|
598
|
+
elif tool_rules_solver.is_continue_tool(tool_call_name):
|
|
599
|
+
continue_stepping = True
|
|
600
|
+
heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing: continue tool rule."
|
|
601
|
+
|
|
602
|
+
# Check if we're at max steps
|
|
603
|
+
if is_final_step and continue_stepping:
|
|
604
|
+
continue_stepping = False
|
|
605
|
+
stop_reason = LettaStopReason(stop_reason=StopReasonType.max_steps.value)
|
|
606
|
+
else:
|
|
607
|
+
uncalled = tool_rules_solver.get_uncalled_required_tools(available_tools=set([t.name for t in agent_state.tools]))
|
|
608
|
+
if not continue_stepping and uncalled:
|
|
609
|
+
continue_stepping = True
|
|
610
|
+
heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing, user expects these tools: [{', '.join(uncalled)}] to be called still."
|
|
611
|
+
stop_reason = None
|
|
612
|
+
|
|
613
|
+
# Create Letta messages from the tool response
|
|
614
|
+
tool_call_messages = create_letta_messages_from_llm_response(
|
|
615
|
+
agent_id=agent_state.id,
|
|
616
|
+
model=agent_state.llm_config.model,
|
|
617
|
+
function_name=tool_call_name,
|
|
618
|
+
function_arguments=tool_args,
|
|
619
|
+
tool_execution_result=tool_result,
|
|
620
|
+
tool_call_id=tool_call_id,
|
|
621
|
+
function_response=function_response_string,
|
|
622
|
+
timezone=agent_state.timezone,
|
|
623
|
+
continue_stepping=continue_stepping,
|
|
624
|
+
heartbeat_reason=heartbeat_reason,
|
|
625
|
+
reasoning_content=reasoning_content,
|
|
626
|
+
pre_computed_assistant_message_id=pre_computed_assistant_message_id,
|
|
627
|
+
is_approval_response=is_approval or is_denial,
|
|
628
|
+
step_id=step_id,
|
|
629
|
+
run_id=run_id,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
# Log step
|
|
633
|
+
step_ns = get_workflow_time_ns() - step_start_time_ns
|
|
634
|
+
await workflow.execute_activity(
|
|
635
|
+
create_step,
|
|
636
|
+
CreateStepParams(
|
|
637
|
+
agent_state=agent_state,
|
|
638
|
+
messages=tool_call_messages,
|
|
639
|
+
actor=actor,
|
|
640
|
+
run_id=run_id,
|
|
641
|
+
step_id=step_id,
|
|
642
|
+
usage=usage,
|
|
643
|
+
step_ns=step_ns,
|
|
644
|
+
llm_request_ns=llm_request_ns,
|
|
645
|
+
tool_execution_ns=tool_execution_ns,
|
|
646
|
+
stop_reason=stop_reason.stop_reason.value if stop_reason else None,
|
|
647
|
+
),
|
|
648
|
+
start_to_close_timeout=CREATE_STEP_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
649
|
+
schedule_to_close_timeout=CREATE_STEP_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
messages_to_persist = initial_messages + tool_call_messages
|
|
653
|
+
|
|
654
|
+
# Persist messages to database
|
|
655
|
+
persisted_messages_result = await workflow.execute_activity(
|
|
656
|
+
create_messages,
|
|
657
|
+
CreateMessagesParams(
|
|
658
|
+
messages=messages_to_persist,
|
|
659
|
+
actor=actor,
|
|
660
|
+
project_id=agent_state.project_id,
|
|
661
|
+
template_id=agent_state.template_id,
|
|
662
|
+
),
|
|
663
|
+
start_to_close_timeout=CREATE_MESSAGES_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
|
664
|
+
schedule_to_close_timeout=CREATE_MESSAGES_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
return persisted_messages_result.messages, continue_stepping, stop_reason, last_function_response
|
|
668
|
+
|
|
669
|
+
async def _get_valid_tools(self, agent_state: AgentState, tool_rules_solver: ToolRulesSolver, last_function_response: str):
|
|
670
|
+
tools = agent_state.tools
|
|
671
|
+
valid_tool_names = tool_rules_solver.get_allowed_tool_names(
|
|
672
|
+
available_tools=set([t.name for t in tools]),
|
|
673
|
+
last_function_response=last_function_response,
|
|
674
|
+
error_on_empty=False, # Return empty list instead of raising error
|
|
675
|
+
) or list(set(t.name for t in tools))
|
|
676
|
+
allowed_tools = [enable_strict_mode(t.json_schema) for t in tools if t.name in set(valid_tool_names)]
|
|
677
|
+
terminal_tool_names = {rule.tool_name for rule in tool_rules_solver.terminal_tool_rules}
|
|
678
|
+
allowed_tools = runtime_override_tool_json_schema(
|
|
679
|
+
tool_list=allowed_tools,
|
|
680
|
+
response_format=agent_state.response_format,
|
|
681
|
+
request_heartbeat=True,
|
|
682
|
+
terminal_tools=terminal_tool_names,
|
|
683
|
+
)
|
|
684
|
+
return allowed_tools
|
|
685
|
+
|
|
686
|
+
def _map_exception_to_stop_reason(self, exc: Exception) -> StopReasonType:
|
|
687
|
+
"""Map activity/workflow exceptions to a StopReasonType for run updates."""
|
|
688
|
+
try:
|
|
689
|
+
if isinstance(exc, ActivityError) and getattr(exc, "cause", None) is not None:
|
|
690
|
+
return self._map_exception_to_stop_reason(exc.cause) # type: ignore[arg-type]
|
|
691
|
+
|
|
692
|
+
if isinstance(exc, ApplicationError):
|
|
693
|
+
err_type = (exc.type or "").strip()
|
|
694
|
+
if err_type in ("ValueError", "LLMJSONParsingError"):
|
|
695
|
+
return StopReasonType.invalid_llm_response
|
|
696
|
+
if err_type == "ContextWindowExceededError":
|
|
697
|
+
return StopReasonType.invalid_llm_response
|
|
698
|
+
if err_type.startswith("LLM") or err_type.endswith("Error"):
|
|
699
|
+
return StopReasonType.llm_api_error
|
|
700
|
+
return StopReasonType.error
|
|
701
|
+
except Exception:
|
|
702
|
+
return StopReasonType.error
|
|
703
|
+
|
|
704
|
+
return StopReasonType.error
|