这是基于公开资料的能力与数据流推演图,不是 Manus 内部私有微服务拓扑。点击节点查看解释,开启“运行数据流”观察一次任务如何从输入走向产物;切换底部标签可对比 Agent Loop、端云协同以及端侧沙箱、Capability Broker 以及分级记忆与长任务管理伪代码。
用户任务先进入 Task / Project 语义容器,再由控制逻辑把模型调用、工具执行、等待事件和恢复串起来。公开资料没有给出内部服务名,因此图中 Orchestrator 与 Model Gateway 使用虚线,表示合理的系统分层推断。
Sandbox 不是普通代码运行器,而是具备网络、文件系统、浏览器和软件工具的隔离云 VM。它让 Agent 能执行脚本、访问网页、生成网站和持续产出文件。
Manus 官方明确提出用文件系统承载长期上下文:网页内容可以从当前上下文移出,但保留 URL;文档可以从上下文省略,但保留路径。todo.md 则把全局计划重新写回注意力末端。
Skills 提供可复用操作手册,Connectors 连接外部数据,Wide Research 通过并行 VM 和通用子 Agent 扩大计算规模。三者分别对应知识复用、数据接入和横向扩展。
ReAct 的经典循环是 Thought → Action → Observation。模型通过行动访问外部环境,再根据观察调整后续计划。Manus 的 Agent Loop 保留了这一认知骨架,但把它放进了更完整的运行时:上下文、工具状态、文件系统和云端执行环境共同参与下一步决策。
ReAct 主要回答“模型如何边推理边行动”;Manus 还要回答“如何让这个循环低成本、可恢复、可扩展地运行”。因此 Manus 把 KV-cache、工具约束、外部化记忆、错误轨迹和并行 Sandbox 纳入核心设计。
端侧沙箱的目标不是复刻云 VM,而是把高风险能力包在受控执行器后面。默认使用进程隔离或 WebAssembly;高风险原生能力通过 Capability Broker 暴露,所有调用都携带 capability token、用户授权、超时和资源配额。
长任务不应依赖一个常驻进程或一次 HTTP 请求。每个步骤都要有稳定的 task_id、step_id、输入摘要、输出引用、幂等键和 checkpoint。
本地层:使用进程内 Event Bus 或 ADK Event Loop 传递 Agent、Tool、Service 事件。低延迟、无需序列化,不适合拆成网络微服务。
边缘网关:用一个 Sync / Transport Gateway 负责鉴权、断线重连、事件序号、压缩、加密和 ACK。长连接可选 WebSocket / QUIC;请求响应可用 HTTP/2。
云端层:只有需要跨设备、跨服务、异步 fan-out 或高吞吐时才接入 durable event bus。不要用消息总线替代任务状态库。
A2A 的位置:当端侧 Agent 要调用“独立部署的云 Agent”时,用 A2A 或等价的远程 Agent 协议;当只是调用本地函数或同进程子 Agent 时,直接函数 / local sub-agent 更简单。
数据平面与控制平面分离:事件和任务控制走同步通道,大文件走 Artifact Store / 分块上传,不要把二进制内容塞进每个事件。
总线消息最小格式:task_id · step_id · event_id · epoch · source · target · capability · payload_ref · deadline · idempotency_key
推荐入口链路:
端侧模型适合做低延迟、隐私优先、频繁调用的工作:意图分类、工具参数补全、短规划、设备控制和本地记忆检索。云端大模型适合处理长上下文、复杂规划、跨文档综合和高风险决策复核。
建议:给端侧模型固定工具 schema、短上下文和严格输出约束;复杂任务由 Placement 层升级,不要依赖端侧模型自行判断所有系统级路由。
Local App → Placement Policy → Local Runner → Event Log → Capability Broker → Sandbox / Tools → Artifact Store
↘ Sync Gateway ↔ Cloud Runner / A2A Agent
enum Capability {
FILE_READ, FILE_WRITE,
NET_CONNECT, PROCESS_EXEC,
CAMERA, MICROPHONE, NOTIFY
}
type TaskPolicy = {
taskId: String
userId: String
capabilities: Set<Capability>
allowedPaths: List<PathRule>
allowedHosts: List<HostRule>
maxCpuMs: Long
maxMemoryMb: Int
maxDiskMb: Int
maxWallTimeMs: Long
requireApproval: Set<Capability>
allowCloudHandoff: Boolean
}
type CapabilityToken = {
tokenId: String
taskId: String
capability: Capability
scope: Json
expiresAt: Instant
nonce: Bytes
signature: Bytes
}
type ToolRequest = {
taskId: String
stepId: String
toolName: String
args: Json
tokenId: String?
idempotencyKey: String
}function buildPolicy(task): TaskPolicy {
risk = classifyRisk(task.goal, task.tools)
// 默认:无网络、无原生进程、只读任务目录
p = TaskPolicy(
taskId = task.id,
userId = task.userId,
capabilities = { FILE_READ },
allowedPaths = [
PathRule("workspace/" + task.id,
read=true, write=true)
],
allowedHosts = [],
maxCpuMs = 30_000,
maxMemoryMb = 512,
maxDiskMb = 256,
maxWallTimeMs = 120_000,
requireApproval = {},
allowCloudHandoff = true
)
if task.needsWeb:
p.capabilities += NET_CONNECT
p.allowedHosts = normalizeAllowlist(task.hosts)
if risk >= HIGH:
p.requireApproval += { FILE_WRITE, NET_CONNECT,
PROCESS_EXEC }
return p
}注意:路径必须 canonicalize 后再匹配,不能用字符串前缀判断;域名解析、重定向和代理也必须重新检查。
class CapabilityBroker {
policyStore: PolicyStore
approval: ApprovalService
signer: KeyStore
audit: AuditLog
async function authorize(req, ctx)
: Result<CapabilityToken> {
policy = policyStore.get(ctx.taskId)
cap = toolToCapability(req.toolName)
if cap not in policy.capabilities:
audit.deny(req, "capability_not_granted")
return Denied
scope = deriveScope(cap, req.args, policy)
if scope.invalid:
audit.deny(req, "scope_out_of_policy")
return Denied
if cap in policy.requireApproval:
decision = await approval.request(
taskId=ctx.taskId, summary=redact(req),
risk=cap, timeoutMs=60_000)
if decision != APPROVED:
audit.deny(req, "user_not_approved")
return Denied
token = CapabilityToken(
tokenId=randomId(), taskId=ctx.taskId,
capability=cap, scope=scope,
expiresAt=now() + 60.seconds,
nonce=randomBytes(32), signature=null)
token.signature = signer.sign(canonicalJson(token))
audit.grant(req, token)
return Granted(token)
}
}async function execute(req, ctx): ToolResult {
await rateLimiter.check(ctx.taskId)
await replayGuard.check(req.idempotencyKey)
decision = await authorize(req, ctx)
if decision is Denied:
return ToolError("DENIED", decision.reason)
token = decision.token
if !signer.verify(token):
return ToolError("INVALID_TOKEN")
if token.taskId != ctx.taskId:
return ToolError("TOKEN_TASK_MISMATCH")
if token.expiresAt < now():
return ToolError("TOKEN_EXPIRED")
safeArgs = enforceScope(req.args, token.scope)
audit.start(req, token)
try:
result = await sandbox.run(
capability=token.capability,
scope=token.scope, args=safeArgs,
budget=ctx.policy.budget)
audit.finish(req, result.summary)
return redactForModel(result)
catch e:
audit.error(req, classify(e))
return ToolError("EXECUTION_FAILED")
}async function createSandbox(task, policy): SandboxHandle {
workspace = fs.createPrivateDir(
root="agent-workspaces/", id=task.id,
mode=0700)
// 生产实现:这里应调用 OS / WASM / 容器适配器
handle = await sandboxProvider.create({
rootfs: readOnlyRuntimeImage(),
workdir: workspace,
uid: freshUnprivilegedUid(),
noNewPrivileges: true,
seccompProfile: "agent-minimal",
filesystem: {
readWrite: [workspace],
readOnly: ["/usr", "/tmp/readonly-tools"]
},
network: {
mode: policy.allowedHosts ? "proxy" : "none",
egressAllowlist: policy.allowedHosts,
dnsRecheck: true
},
limits: {
cpuMs: policy.maxCpuMs,
memoryMb: policy.maxMemoryMb,
diskMb: policy.maxDiskMb,
wallTimeMs: policy.maxWallTimeMs,
pids: 64
},
env: minimalEnvironment(task.id)
})
return handle
}
async function destroySandbox(handle) {
await handle.stop(graceMs=2_000)
await handle.collectLogs(to="artifact://audit/")
await handle.destroy()
}type HandoffManifest = {
taskId: String
parentStepId: String
checkpointHash: String
eventCursor: String
policyDigest: String
inputArtifactRefs: List<Uri>
outputArtifactRefs: List<Uri>
allowedCapabilities: Set<Capability>
privacyClass: "LOCAL_ONLY" | "CLOUD_OK"
expiresAt: Instant
}
async function handoffToCloud(ctx): HandoffResult {
if ctx.policy.allowCloudHandoff == false:
return Rejected("policy")
if ctx.privacyClass == LOCAL_ONLY:
return Rejected("privacy")
checkpoint = eventLog.commitCheckpoint(ctx)
refs = artifactStore.uploadEncrypted(
only=checkpoint.referencedArtifacts)
manifest = sign(HandoffManifest(
taskId=ctx.taskId,
parentStepId=ctx.stepId,
checkpointHash=checkpoint.hash,
eventCursor=checkpoint.cursor,
policyDigest=ctx.policy.digest,
inputArtifactRefs=refs.inputs,
outputArtifactRefs=[],
allowedCapabilities=cloudSafeCaps(ctx),
privacyClass=ctx.privacyClass,
expiresAt=now()+10.minutes))
return transport.send("cloud", manifest)
}async function runStep(ctx, step): Event {
stepKey = `${ctx.taskId}:${step.id}`
if eventLog.hasCompleted(stepKey):
return eventLog.getResult(stepKey)
started = eventLog.append({
type: "STEP_STARTED", stepId: step.id,
idempotencyKey: stepKey})
try:
result = await broker.execute(
request=step.toolRequest,
ctx=ctx.withBudget(step.budget))
completed = eventLog.append({
type: "STEP_COMPLETED", stepId: step.id,
resultRef=artifactStore.save(result),
idempotencyKey=stepKey})
return completed
catch e:
failed = eventLog.append({
type: "STEP_FAILED", stepId: step.id,
retryable=isRetryable(e), errorClass=classify(e)})
scheduler.scheduleRetryOrHandoff(failed)
return failed
}enum EventType {
TASK_CREATED, GOAL_CHANGED,
STEP_STARTED, STEP_COMPLETED, STEP_FAILED,
MEMORY_CANDIDATE, MEMORY_CONFIRMED,
CHECKPOINT_CREATED, TASK_PAUSED,
HANDOFF_REQUESTED, HANDOFF_ACCEPTED,
ARTIFACT_SAVED, TASK_COMPLETED
}
type Event = {
eventId: String
taskId: String
seq: Long
type: EventType
createdAt: Instant
payload: Json
payloadRef: Uri?
idempotencyKey: String?
prevHash: String?
hash: String
}
type TaskState = {
taskId: String
status: TaskStatus
goal: String
phase: String
eventCursor: Long
completedSteps: Set<String>
pendingSteps: List<Step>
blockedReasons: List<String>
checkpointId: String?
policyDigest: String
}
type ArtifactRef = {
uri: Uri
sha256: String
mimeType: String
sizeBytes: Long
sensitivity: Sensitivity
}class StorageLayout {
// L0:当前模型调用,永不作为事实源
contextBuilder: ContextBuilder
// L1:当前任务热状态,可从事件重建
sessionDb: SQLite<SessionState>
// L2:append-only canonical event log
eventLog: DurableEventLog
// L3:可删除、可重建的语义索引
vectorIndex: LocalVectorIndex
memoryRecords: SQLite<MemoryRecord>
// L4:大文件、原始输出、checkpoint payload
artifactStore: EncryptedBlobStore
// L5:可选云端副本,不是本地事实源
syncQueue: SQLite<SyncEnvelope>
}
function persistEvent(event): CommitResult {
// 事务:写入事件后才更新 cursor / 热状态
tx = eventLog.beginTransaction()
try:
tx.append(event)
next = reducer.apply(sessionDb.load(event.taskId), event)
tx.upsertSession(next)
tx.commit()
return Committed(next.eventCursor)
catch e:
tx.rollback()
return Failed("DURABLE_WRITE_FAILED")
}async function appendEvent(taskId, type, payload, opts={}): Event {
last = eventLog.last(taskId)
event = Event(
eventId = uuid(), taskId = taskId,
seq = last.seq + 1, type = type,
createdAt = clock.now(), payload = payload,
payloadRef = opts.payloadRef,
idempotencyKey = opts.idempotencyKey,
prevHash = last.hash,
hash = hashCanonical(last.hash, type, payload))
if opts.idempotencyKey:
old = eventLog.findByIdempotency(opts.idempotencyKey)
if old != null: return old
await persistEvent(event)
return event
}
function reduce(state, event): TaskState {
switch event.type:
case STEP_COMPLETED:
state.completedSteps.add(event.payload.stepId)
state.pendingSteps.remove(event.payload.stepId)
case TASK_PAUSED:
state.status = PAUSED
case HANDOFF_ACCEPTED:
state.status = RUNNING_ON_CLOUD
case TASK_COMPLETED:
state.status = COMPLETED
state.eventCursor = event.seq
return state
}type Checkpoint = {
checkpointId: String
taskId: String
eventCursor: Long
stateDigest: String
goalSummary: String
currentPhase: String
completedSteps: List<String>
pendingSteps: List<String>
decisions: List<DecisionRef>
openQuestions: List<String>
memoryRefs: List<String>
artifactRefs: List<ArtifactRef>
policyDigest: String
}
async function createCheckpoint(taskId): Checkpoint {
state = sessionDb.load(taskId)
cp = Checkpoint(
checkpointId=uuid(), taskId=taskId,
eventCursor=state.eventCursor,
stateDigest=hash(state),
goalSummary=summarizeGoal(state),
currentPhase=state.phase,
completedSteps=state.completedSteps,
pendingSteps=state.pendingSteps,
decisions=extractDecisionRefs(taskId),
openQuestions=state.blockedReasons,
memoryRefs=memoryDb.refsForTask(taskId),
artifactRefs=artifactStore.refsForTask(taskId),
policyDigest=state.policyDigest)
ref = await artifactStore.putJson(cp)
await appendEvent(taskId, CHECKPOINT_CREATED,
{checkpointId: cp.checkpointId}, {payloadRef: ref.uri})
return cp
}type MemoryRecord = {
memoryId: String
scope: "task" | "user" | "device"
kind: "fact" | "constraint" | "preference" | "decision" | "lesson"
content: String
embedding: Vector?
embeddingModel: String?
sourceEventId: String
sourceArtifactRef: Uri?
confidence: Float
confirmedByUser: Boolean
validFrom: Instant
validUntil: Instant?
sensitivity: Sensitivity
supersedes: String?
status: "candidate" | "active" | "superseded" | "expired"
}
async function extractMemory(event): List<MemoryCandidate> {
if event.type in {STEP_STARTED, STEP_FAILED}:
return [] // 默认不把噪声写入长期记忆
candidates = localModel.extractStructured(
text=event.payload.text,
schema=MemoryCandidateSchema,
instruction="只提取未来可能复用的事实、约束、决策或经验")
return filter(candidates,
x => x.confidence >= 0.65 and x.hasEvidence)
}
async function promoteMemory(candidate, event): MemoryRecord? {
similar = memoryDb.findSimilar(candidate.content,
filters={scope: candidate.scope, status: "active"})
merged = resolveConflict(similar, candidate)
if merged.requiresUserConfirmation:
return memoryDb.insertCandidate(merged)
record = memoryDb.activate(merged, sourceEventId=event.eventId)
await appendEmbeddingJob(record.memoryId)
return record
}async function retrieveMemory(query, ctx): List<MemoryRecord> {
parsed = queryParser.parse(query)
filters = {
status: "active",
scope: inSet(["user", "device", "task"]),
sensitivity: allowedBy(ctx.policy),
taskId: ctx.taskId or ANY
}
vectorHits = vectorIndex.search(
embed(query), topK=40, filters=filters)
keywordHits = memoryDb.fullTextSearch(
parsed.terms, topK=40, filters=filters)
candidates = deduplicate(vectorHits + keywordHits)
scored = candidates.map(m => ({
memory: m,
score: 0.45 * m.semanticScore
+ 0.20 * m.lexicalScore
+ 0.15 * taskRelevance(m, ctx)
+ 0.10 * m.confidence
+ 0.10 * freshness(m, clock.now())
}))
ranked = rerank(scored, query, ctx)
return diversifyAndBudget(
ranked, maxTokens=ctx.memoryTokenBudget)
}
function freshness(m, now): Float {
halfLife = {
preference: 180.days,
lesson: 30.days,
tool_failure: 14.days,
task_state: INFINITE
}[m.kind]
return validAt(m, now)
? expDecay(now - m.validFrom, halfLife)
: 0.0
}async function buildContext(taskId, step): Context {
state = sessionDb.load(taskId)
recent = eventLog.tail(taskId, maxEvents=20)
memories = await retrieveMemory(
query=step.goal, ctx=state.context)
// 模型看到的是证据包,不是无来源的散文
evidence = memories.map(m => {
id: m.memoryId,
text: m.content,
source: m.sourceEventId,
artifact: m.sourceArtifactRef,
confidence: m.confidence,
validUntil: m.validUntil
})
return Context(
stableSystemPrompt=systemPrompt,
goal=state.goal,
checkpoint=state.checkpointSummary,
activeConstraints=state.activeConstraints,
recentEvents=redact(recent),
retrievedMemory=evidence,
artifactRefs=state.artifactRefs,
toolSchema=toolRegistry.for(step))
}async function resumeTask(taskId): ResumeResult {
cp = checkpointStore.latest(taskId)
if cp == null:
cp = checkpointStore.fromTaskCreated(taskId)
currentPolicy = policyStore.current(taskId)
state = checkpointToState(cp)
tail = eventLog.readAfter(taskId, cp.eventCursor)
for event in tail:
state = reduce(state, event)
if cp.policyDigest != currentPolicy.digest:
state = revalidatePendingSteps(state, currentPolicy)
if state.status in {COMPLETED, CANCELLED}:
return Done(state)
next = planner.nextRunnableStep(state)
ctx = await buildContext(taskId, next)
return runner.resume(
taskId=taskId, state=state,
nextStep=next, context=ctx)
}async function schedulerTick() {
tasks = taskStore.findRunnable(limit=20)
for task in tasks:
device = deviceMonitor.snapshot()
if !resourcePolicy.allows(task, device):
await appendEvent(task.id, TASK_PAUSED,
{reason: resourcePolicy.reason(device)})
continue
lease = leaseStore.acquire(task.id,
owner=device.id, ttl=60.seconds)
if !lease.acquired: continue
try:
step = planner.nextRunnableStep(task.state)
result = await runIdempotentStep(task, step)
if shouldCheckpoint(task, result):
await createCheckpoint(task.id)
finally:
leaseStore.release(lease)
}
async function runIdempotentStep(task, step) {
key = `${task.id}:${step.id}:${step.inputHash}`
old = eventLog.completedByIdempotency(key)
if old: return old.payload
await appendEvent(task.id, STEP_STARTED,
{stepId: step.id}, {idempotencyKey: key})
result = await toolRunner.execute(step)
await appendEvent(task.id, STEP_COMPLETED,
{stepId: step.id, resultRef: save(result)},
{idempotencyKey: key})
return result
}async function compactTask(taskId) {
state = sessionDb.load(taskId)
events = eventLog.all(taskId)
if events.length < 50: return
summary = localModel.summarize({
goal: state.goal,
completed: state.completedSteps,
events: events.slice(-200)
}, schema=StageSummarySchema)
summaryRef = await artifactStore.putJson({
summary, sourceRange: [events[0].seq, events[-1].seq],
sourceHash: merkleRoot(events)
})
// 删除的是热上下文,不是 canonical event log
await sessionDb.setSummary(taskId, summaryRef)
await createCheckpoint(taskId)
await memoryExtractor.promoteFacts(taskId)
return summaryRef
}
function evictionScore(m, now): Float {
if m.confirmedByUser: return -INFINITE
if m.kind == "task_state" and taskOpen(m.taskId):
return -INFINITE
return 0.30 * age(m, now)
+ 0.25 * inverseAccess(m)
+ 0.20 * inverseConfidence(m)
+ 0.15 * storageCost(m)
+ 0.10 * lowReuseProbability(m)
}async function syncOutbox() {
batch = syncQueue.nextBatch(maxBytes=1_000_000)
for item in batch:
if !privacyPolicy.allows(item):
syncQueue.markBlocked(item, "LOCAL_ONLY")
continue
envelope = encryptAndSign({
deviceId: device.id,
taskId: item.taskId,
cursor: item.cursor,
type: item.type,
payloadRef: item.payloadRef,
previousHash: item.previousHash
})
ack = await transport.send(envelope)
if ack.accepted:
syncQueue.ack(item.id, ack.serverCursor)
}
async function mergeCloudDelta(delta) {
verifySignature(delta)
for event in delta.events:
if !eventLog.has(event.eventId):
await persistEvent(event)
for memory in delta.memoryRecords:
memoryDb.merge(resolveConflict(
memoryDb.find(memory.memoryId), memory))
await rebuildDerivedIndexesIfNeeded(delta)
}async function rebuildVectorIndex(modelVersion) {
next = LocalVectorIndex.createTemporary(
modelVersion=modelVersion)
cursor = 0
while true:
batch = memoryDb.activeRecordsAfter(cursor,
limit=128, allowed=privacyPolicy.localIndex())
if batch.empty: break
vectors = await embedder.embed(
batch.map(x => x.content))
next.upsert(batch.zip(vectors))
cursor = batch[-1].rowId
await scheduler.yieldIfNeeded()
next.commitAtomically()
}
async function deleteMemory(memoryId) {
memoryDb.markDeleted(memoryId)
vectorIndex.delete(memoryId)
syncQueue.enqueue({type: "MEMORY_DELETE", memoryId})
// 原始来源事件按审计/合规策略保留或擦除
await audit.record("MEMORY_DELETED", memoryId)
}async function runLongTask(task) {
await appendEvent(task.id, TASK_CREATED,
{goal: task.goal, policyDigest: task.policy.digest})
while !taskStore.isTerminal(task.id):
try:
result = await resumeTask(task.id)
if result.needsApproval:
await waitForUser(task.id)
if result.needsNetwork:
await waitForNetwork(task.id)
if result.needsCloud:
manifest = await handoffToCloud(task.id)
await waitForCloudMerge(manifest)
if result.stepFinished:
await extractAndPromoteMemory(result.events)
if shouldCheckpoint(task.id):
await createCheckpoint(task.id)
if storageNearLimit():
await compactTask(task.id)
catch CrashOrShutdown:
// 进程可直接退出;事件已落盘,下一次 tick 会恢复
return Paused
await appendEvent(task.id, TASK_COMPLETED,
{finalArtifacts: artifactStore.refsForTask(task.id)})
await memoryExtractor.commitReusableLessons(task.id)
}因此本图适合做产品理解、技术交流和竞品分析;不应当作为 Manus 内部部署图或安全合规证明。
注:日期和产品能力会随版本变化;本页面采用 2026-10-24 可访问的公开资料,内部实现部分以“结构推断”明确标注。