
AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆【免费下载链接】OperitThe most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent项目地址https://gitcode.com/gh_mirrors/op/Operit点击查看免费下载Operit 通过既有外部 HTTP 服务对外提供 A2A 1.0 Server 能力其中最关键的一环是把 A2A 协议的任务Task安全地映射到 Operit 内部独立的流式聊天执行上。本文基于仓库中的 A2A Server 任务生命周期设计文档 及其配套的 协议边界文档、文档与验证记录结合 A2aTaskManager.kt 与 A2aHttpHandler.kt 的完整实现逐层拆解服务端 task ID 生成、上下文与独立聊天的绑定、任务状态机、returnImmediately语义、SSE 流式映射以及查询与取消机制。读完本文你将掌握 Operit A2A 任务从提交到终态的完整生命周期并可直接对照源码与 A2A Server 协议文档 进行二次开发或联调。一、任务生命周期在 A2A Server 中的定位A2AAgent2Agent1.0 协议要求 Server 对每次调用暴露一个可寻址的 Task 资源客户端提交消息、查询任务状态、订阅事件流并可随时取消。Operit 的设计约束见 index.md要求复用既有聊天执行器但将每个 A2A 请求隔离为独立聊天与独立任务A2A 调用不共享、不篡改用户正在进行的普通聊天任务只存活于外部 HTTP 服务运行期间客户端可以查询和取消服务仍存活期间的任务。围绕这三个约束任务生命周期层integrations/a2a/下的A2aTaskManager承担了全部执行管理职责而协议序列化JSON-RPC 封装、SSE 编码由A2aHttpHandler负责。二者分工清晰A2aTaskManager只管理执行不关心线协议源码注释原文The A2A protocol layer owns serialization; this class only manages execution.见 A2aTaskManager.kt。二、服务端 task ID 生成与任务注册每个 A2A 请求进入系统后第一步是生成服务端唯一的 task ID。A2aTaskManager.submit()的实现如下A2aTaskManager.ktfun submit(message: A2aIncomingMessage): A2aTaskSnapshot { val contextId resolveContextId(message) val chatId contextChats[contextId] val taskId UUID.randomUUID().toString() val record TaskRecord(taskId, contextId) tasks[taskId] record val job serviceScope.launch(Dispatchers.IO) { executeTask(record, message.text, chatId) } record.attachJob(job) return record.snapshot() }要点拆解task ID 采用UUID.randomUUID()生成与服务端既有聊天 ID、上下文 ID 完全解耦客户端拿到的result.task.id就是这个值任务注册在ConcurrentHashMapString, TaskRecordtasks中天然支持并发提交与并发查询每个任务持有一个Jobrecord.attachJob(job)使取消任务可以直接取消底层协程submit立即返回record.snapshot()此时状态为TASK_STATE_SUBMITTED后续真正的执行在Dispatchers.IO上异步进行。注意任务记录是纯内存态的文档明确Tasks are held in memory for the lifetime of the external HTTP serviceexternal_a2a_server.md服务重启或停用后任务记录即被清空且 A2A 任务状态不做持久化。三、上下文 ID 与独立 Operit 聊天的映射3.1 上下文解析没有 contextId 则自动生成private fun resolveContextId(message: A2aIncomingMessage): String { return message.contextId?.trim()?.takeIf { it.isNotBlank() } ?: UUID.randomUUID().toString() }A2aTaskManager.kt请求若携带非空contextId则原样采用会做 trim 与空白校验否则为本次任务生成全新上下文。这正是 A2A 1.0 的语义contextId表示同一会话的延续标识。3.2 一个上下文对应一个 Operit 聊天映射表是contextChats: ConcurrentHashMapString, StringA2aTaskManager.kt键为 A2AcontextId值为 Operit 内部聊天 ID。映射策略在executeTask中落地val request ExternalChatRequest( requestId record.id, message message, createNewChat existingChatId null, chatId existingChatId, createIfNone false, returnToolStatus false )A2aTaskManager.kt首个请求contextChats中没有该上下文对应的聊天existingChatId null于是createNewChat true为这个 A2A 上下文创建一条全新的 Operit 聊天后续请求同一上下文的新消息携带相同contextId从contextChats命中既有聊天 IDchatId existingChatId、createNewChat false在同一条聊天里续写流式会话启动成功后contextChats[record.contextId] session.chatId完成绑定A2aTaskManager.kt。这套映射就是每个新的 A2A 上下文创建独立 Operit 聊天后续关联到同一上下文的新请求复用对应聊天这一设计目标的源码级实现对应 02_task_lifecycle.md 修改项第 2 条。3.3 与普通聊天的隔离由于 A2A 任务只会操作自己上下文专属的新聊天createIfNone false且绝不向既有普通聊天传chatId因此A2A 调用不会共享或篡改用户正在进行的普通聊天——这是 02_task_lifecycle.md 预期结果的第一条也是任务隔离约束index.md 意图部分的直接体现。此外从 01_protocol_boundary.md 可以确认A2A 与既有 REST 路由只共享 HTTP Server、Token 与聊天执行器不共享协议对象。四、任务状态机从提交到终态任务状态常量定义在 A2aTaskManager.kt状态常量值是否终态TASK_STATE_SUBMITTEDTASK_STATE_SUBMITTED否TASK_STATE_WORKINGTASK_STATE_WORKING否TASK_STATE_INPUT_REQUIREDTASK_STATE_INPUT_REQUIRED否TASK_STATE_AUTH_REQUIREDTASK_STATE_AUTH_REQUIRED否TASK_STATE_COMPLETEDTASK_STATE_COMPLETED是TASK_STATE_CANCELEDTASK_STATE_CANCELED是TASK_STATE_FAILEDTASK_STATE_FAILED是TASK_STATE_REJECTEDTASK_STATE_REJECTED是isTerminalState()判定COMPLETED、CANCELED、FAILED、REJECTED四种终态。实际执行路径中A2aTaskManager.kt用到的主要迁移为SUBMITTED → WORKINGexecutor.startStreaming(request)返回Started后record.start(session)将状态置为WORKING并向订阅者发布非 final 的Status事件L199-L214WORKING → COMPLETED响应流收集完毕session.responseStreamSession.currentState()不是InputProcessingState.Error时record.complete()任意非终态 → FAILED启动失败ExternalChatStreamingStartResult.Failed或最终状态为Error、或执行过程抛异常时record.fail(message)任意非终态 → CANCELED客户端调用CancelTask、或服务关闭时cancelForShutdown()触发。TaskRecord用一把synchronized(lock)保护所有状态迁移与输出追加保证并发场景一边流式写、一边查询/取消下状态一致。output以字符串累加方式保存全部文本输出A2aTaskSnapshot每次快照都携带id、contextId、state、output与可选的errorL312-L320。值得注意的边界Operit 不实现TASK_STATE_INPUT_REQUIRED即不接受带taskId的消息续写协议文档与parseIncomingMessage都会拒绝这类消息A2aHttpHandler.kt。五、SendMessage 与 returnImmediately 语义SendMessage的行为由configuration.returnImmediately决定A2aHttpHandler.ktMETHOD_SEND_MESSAGE - { val sendRequest parseSendMessageRequest(request.params) val submittedTask taskManager.submit(sendRequest.message) val resultTask if (sendRequest.returnImmediately) { submittedTask } else { runBlocking { taskManager.awaitTerminalTask(submittedTask.id) } } jsonRpcResultResponse(request.id, JSONObject().put(task, taskToJson(resultTask))) }returnImmediately false默认JSON-RPC 请求会阻塞等待任务进入终态再返回完整结果客户端一次请求即拿到最终task含artifacts文本returnImmediately trueSendMessage立即返回提交瞬间的任务快照通常是TASK_STATE_WORKING客户端随后用GetTask轮询推进。awaitTerminalTask底层是TaskRecord.awaitTerminal()A2aTaskManager.kt若任务已终态则直接返回快照否则挂起在CompletableDeferredA2aTaskSnapshot上由complete/fail/cancel等终态迁移来补全。这正对应 02_task_lifecycle.md 修改项第 3 条SendMessage按 A2A 1.0 的returnImmediately语义返回任务后台将流式聊天执行映射为工作、完成、失败或取消状态。请求参数校验parseSendMessageRequestA2aHttpHandler.kt还会拒绝taskPushNotificationConfig抛出A2aPushNotificationNotSupportedException错误码-32003、校验acceptedOutputModes必须包含text/plain否则-32005并接受可选的historyLength。消息本身要求ROLE_USER、非空messageId、至少一个纯文本 Part{text: ...}多 Part 会以换行拼接L353-L387。六、流式任务SendStreamingMessage 与 SubscribeToTask6.1 SSE 管道建立SendStreamingMessage提交任务后立即返回text/event-stream响应SubscribeToTask对已存在的活动任务建立同样的流。两者的共同实现是streamingResponse()A2aHttpHandler.kt用PipedInputStream(64 * 1024)/PipedOutputStream连接协程事件流与 HTTP 响应体通过ChannelA2aTaskEvent(Channel.UNLIMITED)转发任务事件订阅时requireActive控制SendStreamingMessage允许终态任务任务可能已经瞬间完成SubscribeToTask则要求任务处于活动状态否则抛出A2aUnsupportedOperationException-32004cannot be subscribed after it is terminal响应头附带A2A-Version: 1.0、Cache-Control: no-cache、Connection: keep-alive、X-Accel-Buffering: no并在ExternalChatHttpServer.useGzipWhenAccepted中对 SSE 响应禁用 gzipExternalChatHttpServer.kt保证逐帧推送不被缓冲。6.2 事件序列initial task → artifactUpdate → statusUpdate(final:true)每个data:行都是一个带 JSON-RPC envelope 的 A2A 1.0 Stream Response每条数据只有一个成员写入时对多行 JSON 逐行加data:前缀writeSseEventL542-L551。典型事件流data: {jsonrpc:2.0,id:2,result:{task:{id:task-uuid,contextId:context-uuid,status:{state:TASK_STATE_WORKING}}}} data: {jsonrpc:2.0,id:2,result:{artifactUpdate:{taskId:task-uuid,contextId:context-uuid,artifact:{artifactId:task-uuid-result,parts:[{text:这是回答的第一部分。}]},append:true,lastChunk:false}}} data: {jsonrpc:2.0,id:2,result:{statusUpdate:{taskId:task-uuid,contextId:context-uuid,status:{state:TASK_STATE_COMPLETED},final:true}}}事件映射规则见 A2aHttpHandler.kt首个事件携带result.task当前快照输出增量映射为result.artifactUpdateartifactId固定为$taskId-resultappend: true、lastChunk: false状态变化映射为result.statusUpdate携带taskId、contextId、status.state终态时附带含message的Messagerole 为ROLE_AGENT与final标记收到Status事件且final true后流关闭L232-L240。流式收集侧A2aTaskManager.executeTask通过ExternalChatResponseSanitizer.sanitizeStream(...)清洗流式响应后逐块record.appendOutput(chunk)每块追加都会发布一条Artifact事件A2aTaskManager.kt——这就是后台将流式聊天执行映射为工作、完成、失败或取消状态的完整链路。七、快照查询、分页列表与取消7.1 GetTask即时快照GetTask从tasks表取出TaskRecord并返回snapshot()A2aTaskManager.kt。任务不存在时抛A2aTaskNotFoundExceptionJSON-RPC 错误码-32001。7.2 ListTasks过滤与分页listTasks(contextId, state)A2aTaskManager.kt支持按上下文与状态过滤、按任务 ID 排序然后由 listTasksToJson 做游标分页pageSize默认为 50上限 100DEFAULT_TASK_LIST_PAGE_SIZE 50、MAX_TASK_LIST_PAGE_SIZE 100pageToken是上一页最后一个任务的 IDindexOfFirst { task - task.id token } 1作为下一页起始token 非法时抛-32602还有剩余任务时返回nextPageToken取当前页最后一个任务 IDstatus参数必须落在VALID_TASK_STATES集合内。7.3 CancelTask协同取消cancelTask(taskId)调TaskRecord.cancel()A2aTaskManager.kttransitionToCancelled()在锁内把非终态任务置为CANCELED并记录错误信息对活动中的流式会话调用responseStreamSession.cancel()中断响应流取消底层协程job.cancel()补全terminalTask让阻塞等待SendMessage的客户端解除挂起发布final: true的Status事件SSE 订阅端据此关闭流。已处终态的任务调用CancelTask会抛A2aTaskNotCancelableException-32002。executeTask捕获到协程CancellationException时也会markCancelled()并向上抛出L155-L157保证取消路径状态一致。取消后的任务快照即为返回结果状态为TASK_STATE_CANCELED。八、生命周期边界与清理8.1 服务存活期内的任务A2aTaskManager与外部 HTTP 服务同生命周期ExternalChatHttpServer构造时创建A2aHttpHandler进而创建A2aTaskManagerstopServer()时调用a2aHandler.close()ExternalChatHttpServer.kt。A2aTaskManager.close()L109-L113会对所有未终态任务执行cancelForShutdown()与主动取消相同中断会话、取消协程、发布 final 事件清空tasks与contextChats两张表。因此客户端可以查询和取消服务仍存活期间的任务02_task_lifecycle.md 预期结果第 2 条成立服务停止后任务记录随内存释放而消失不做持久化。8.2 路由与协议边界A2A 入口挂在既有serve()分发链上且优先级先于健康检查与 REST 路由ExternalChatHttpServer.ktsession.uri A2aHttpHandler.AGENT_CARD_PATH - a2aHandler.handleAgentCard(session).withCors() session.uri A2aHttpHandler.A2A_PATH - a2aHandler.handleJsonRpc(session).withCors()GET /.well-known/agent-card.jsonAGENT_CARD_PATH无需 Bearer Token从请求Host头动态拼出本次可访问的/a2aJSON-RPC URLbuildJsonRpcEndpointL483-L492并声明protocolVersion: 1.0、JSONRPCbinding、流式能力pushNotifications: false、text/plain输入输出模式与 Bearer 安全方案POST /a2a除OPTIONS外全部走既有requireBearerToken鉴权并校验A2A-Version请求头非1.0返回VersionNotSupportedError-32009L471-L481。这印证了 01_protocol_boundary.md 的设计协议解析与任务管理收敛在integrations/a2a/既有/api/health、/api/external-chat、/api/web/*与 Web Chat 路由保持原样。九、能力边界速查根据 external_a2a_server.md 与源码A2A Server 的能力边界为输入/输出模式仅text/plain不接受文件、结构化数据 Part推送通知不支持Agent Card 声明pushNotifications: false请求带taskPushNotificationConfig即报-32003续写不支持带taskId的消息无TASK_STATE_INPUT_REQUIRED历史与持久化不返回任务历史任务状态不跨服务重启持久化客户端本模块不提供 A2A Client且 A2A 路由不并入 MCP。联调时请以 A2A Server 协议文档 中的 curl/JSON 示例为准它同时涵盖了 Agent Card 发现、Bearer 鉴权、六种 JSON-RPC 方法及 SSE 事件格式的完整范例。赞分享AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆【免费下载链接】OperitThe most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent项目地址https://gitcode.com/gh_mirrors/op/Operit点击查看免费下载相关推荐A2A 协议任务生命周期深度解析从 Message 到 Task 的状态流转与多轮协作实战A2A 协议任务生命周期深度解析从 Message 到 Task 的状态流转与多轮协作实战 导读 Agent2AgentA2A协议中一次交互既可以是一次人工智能AI AgentAPI设计A2A协议Task生命周期管理从提交到完成的完整工作流解析A2A协议Task生命周期管理从提交到完成的完整工作流解析 A2A协议Agent2Agent Protocol是谷歌开源的首个标准智能体交互协议为AI智人工智能AI AgentAPI设计Operit A2A Server 实现指南在 Android 外部 HTTP 服务上接入 A2A 1.0 协议与 JSON-RPC 任务生命周期Operit A2A Server 实现指南在 Android 外部 HTTP 服务上接入 A2A 1.0 协议与 JSON RPC 任务生命周期 本指南基于AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化上一篇Ingress NGINX 注解风险等级与作用域全解annotations-risk 治理指南下一篇Flipper Zero 手电筒插件Flashlight源码剖析与 GPIO 控制实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考